summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAhmad Hassan <ahmad.hassan612@gmail.com>2018-11-15 16:19:09 +0200
committerAhmad Hassan <ahmad.hassan612@gmail.com>2018-11-15 16:19:09 +0200
commit73b3da73d62b3b18b6552f2a99f86458d03d628a (patch)
treead65dac4757615ddb813f6152d4eb76ac14ebee4
parentebc174e968ece65110aa722a18cdf437cfa75eeb (diff)
parentbf31bd0d82affc29963d7d15c850ae0a0cc362a3 (diff)
downloadgitlab-ce-73b3da73d62b3b18b6552f2a99f86458d03d628a.tar.gz
Merge remote-tracking branch 'origin/master' into support-gitaly-tls
-rw-r--r--.gitattributes1
-rw-r--r--.gitlab-ci.yml194
-rw-r--r--.gitlab/CODEOWNERS.disabled (renamed from .gitlab/CODEOWNERS)0
-rw-r--r--.nvmrc2
-rw-r--r--.ruby-version2
-rw-r--r--CHANGELOG.md35
-rw-r--r--Dockerfile.assets4
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile18
-rw-r--r--Gemfile.lock185
-rw-r--r--Gemfile.rails4 (renamed from Gemfile.rails5)4
-rw-r--r--Gemfile.rails4.lock (renamed from Gemfile.rails5.lock)187
-rw-r--r--app/assets/images/cluster_app_logos/knative.pngbin0 -> 11259 bytes
-rw-r--r--app/assets/javascripts/badges/components/badge.vue2
-rw-r--r--app/assets/javascripts/badges/components/badge_form.vue2
-rw-r--r--app/assets/javascripts/badges/components/badge_list.vue2
-rw-r--r--app/assets/javascripts/badges/components/badge_list_row.vue2
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js2
-rw-r--r--app/assets/javascripts/boards/components/board.js27
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.vue15
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue128
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue3
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js41
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.vue362
-rw-r--r--app/assets/javascripts/boards/components/issue_due_date.vue90
-rw-r--r--app/assets/javascripts/boards/components/issue_time_estimate.vue48
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.vue4
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.vue16
-rw-r--r--app/assets/javascripts/boards/components/modal/header.vue80
-rw-r--r--app/assets/javascripts/boards/components/modal/index.vue231
-rw-r--r--app/assets/javascripts/boards/components/modal/list.vue198
-rw-r--r--app/assets/javascripts/boards/components/modal/tabs.vue30
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js43
-rw-r--r--app/assets/javascripts/boards/components/project_select.vue8
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.vue122
-rw-r--r--app/assets/javascripts/boards/filtered_search_boards.js9
-rw-r--r--app/assets/javascripts/boards/index.js6
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js10
-rw-r--r--app/assets/javascripts/boards/models/issue.js32
-rw-r--r--app/assets/javascripts/boards/models/list.js4
-rw-r--r--app/assets/javascripts/boards/services/board_service.js12
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js57
-rw-r--r--app/assets/javascripts/boards/stores/modal_store.js12
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js15
-rw-r--r--app/assets/javascripts/clusters/clusters_index.js24
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue73
-rw-r--r--app/assets/javascripts/clusters/constants.js8
-rw-r--r--app/assets/javascripts/clusters/services/clusters_service.js1
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js13
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue19
-rw-r--r--app/assets/javascripts/commons/gitlab_ui.js17
-rw-r--r--app/assets/javascripts/commons/index.js1
-rw-r--r--app/assets/javascripts/commons/polyfills.js1
-rw-r--r--app/assets/javascripts/deploy_keys/components/action_btn.vue4
-rw-r--r--app/assets/javascripts/deploy_keys/components/app.vue2
-rw-r--r--app/assets/javascripts/diffs/components/app.vue18
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue32
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue6
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions_dropdown.vue24
-rw-r--r--app/assets/javascripts/diffs/components/diff_content.vue84
-rw-r--r--app/assets/javascripts/diffs/components/diff_discussions.vue55
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue38
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue56
-rw-r--r--app/assets/javascripts/diffs/components/diff_gutter_avatars.vue4
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_gutter_content.vue16
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_note_form.vue6
-rw-r--r--app/assets/javascripts/diffs/components/diff_table_cell.vue4
-rw-r--r--app/assets/javascripts/diffs/components/image_diff_overlay.vue139
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_comment_row.vue2
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_table_row.vue8
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_view.vue8
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue14
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_table_row.vue8
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_view.vue6
-rw-r--r--app/assets/javascripts/diffs/components/tree_list.vue6
-rw-r--r--app/assets/javascripts/diffs/constants.js1
-rw-r--r--app/assets/javascripts/diffs/index.js5
-rw-r--r--app/assets/javascripts/diffs/store/actions.js20
-rw-r--r--app/assets/javascripts/diffs/store/getters.js17
-rw-r--r--app/assets/javascripts/diffs/store/modules/diff_state.js2
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js4
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js108
-rw-r--r--app/assets/javascripts/diffs/store/utils.js148
-rw-r--r--app/assets/javascripts/dirty_submit/dirty_submit_form.js3
-rw-r--r--app/assets/javascripts/environments/components/container.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.vue42
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.vue9
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue61
-rw-r--r--app/assets/javascripts/environments/components/environment_monitoring.vue8
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.vue17
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.vue14
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.vue7
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue152
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue2
-rw-r--r--app/assets/javascripts/environments/components/stop_environment_modal.vue6
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js30
-rw-r--r--app/assets/javascripts/files_comment_button.js2
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_user.js7
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown.js2
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js27
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js113
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js9
-rw-r--r--app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js14
-rw-r--r--app/assets/javascripts/flash.js8
-rw-r--r--app/assets/javascripts/frequent_items/components/app.vue2
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js63
-rw-r--r--app/assets/javascripts/gl_field_errors.js2
-rw-r--r--app/assets/javascripts/group.js12
-rw-r--r--app/assets/javascripts/groups/components/app.vue2
-rw-r--r--app/assets/javascripts/ide/components/branches/search_list.vue2
-rw-r--r--app/assets/javascripts/ide/components/error_message.vue4
-rw-r--r--app/assets/javascripts/ide/components/file_templates/dropdown.vue2
-rw-r--r--app/assets/javascripts/ide/components/ide.vue4
-rw-r--r--app/assets/javascripts/ide/components/ide_side_bar.vue4
-rw-r--r--app/assets/javascripts/ide/components/jobs/list.vue2
-rw-r--r--app/assets/javascripts/ide/components/jobs/stage.vue2
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/list.vue2
-rw-r--r--app/assets/javascripts/ide/components/pipelines/list.vue2
-rw-r--r--app/assets/javascripts/ide/components/preview/clientside.vue2
-rw-r--r--app/assets/javascripts/ide/components/preview/navigator.vue2
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue4
-rw-r--r--app/assets/javascripts/ide/stores/getters.js2
-rw-r--r--app/assets/javascripts/jobs/components/artifacts_block.vue14
-rw-r--r--app/assets/javascripts/jobs/components/commit_block.vue10
-rw-r--r--app/assets/javascripts/jobs/components/empty_state.vue9
-rw-r--r--app/assets/javascripts/jobs/components/erased_block.vue6
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue334
-rw-r--r--app/assets/javascripts/jobs/components/job_container_item.vue21
-rw-r--r--app/assets/javascripts/jobs/components/job_log.vue74
-rw-r--r--app/assets/javascripts/jobs/components/job_log_controllers.vue44
-rw-r--r--app/assets/javascripts/jobs/components/sidebar.vue31
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_detail_row.vue9
-rw-r--r--app/assets/javascripts/jobs/components/stuck_block.vue8
-rw-r--r--app/assets/javascripts/jobs/components/trigger_block.vue10
-rw-r--r--app/assets/javascripts/jobs/index.js1
-rw-r--r--app/assets/javascripts/jobs/mixins/delayed_job_mixin.js50
-rw-r--r--app/assets/javascripts/jobs/store/actions.js4
-rw-r--r--app/assets/javascripts/jobs/store/getters.js9
-rw-r--r--app/assets/javascripts/labels_select.js248
-rw-r--r--app/assets/javascripts/lib/utils/ace_utils.js4
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js12
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js2
-rw-r--r--app/assets/javascripts/members.js10
-rw-r--r--app/assets/javascripts/merge_request_tabs.js3
-rw-r--r--app/assets/javascripts/milestone_select.js5
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue21
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue61
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue30
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter.vue23
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue71
-rw-r--r--app/assets/javascripts/notes/components/note_awards_list.vue29
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue12
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue12
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue205
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue12
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue18
-rw-r--r--app/assets/javascripts/notes/components/toggle_replies_widget.vue94
-rw-r--r--app/assets/javascripts/notes/constants.js2
-rw-r--r--app/assets/javascripts/notes/discussion_filters.js12
-rw-r--r--app/assets/javascripts/notes/stores/actions.js25
-rw-r--r--app/assets/javascripts/notes/stores/collapse_utils.js2
-rw-r--r--app/assets/javascripts/notes/stores/getters.js2
-rw-r--r--app/assets/javascripts/notes/stores/modules/index.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js8
-rw-r--r--app/assets/javascripts/pages/groups/clusters/destroy/index.js5
-rw-r--r--app/assets/javascripts/pages/groups/clusters/index/index.js5
-rw-r--r--app/assets/javascripts/pages/groups/clusters/show/index.js5
-rw-r--r--app/assets/javascripts/pages/groups/clusters/update/index.js5
-rw-r--r--app/assets/javascripts/pages/groups/index.js16
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue99
-rw-r--r--app/assets/javascripts/pages/projects/clusters/gcp/new/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/clusters/index/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/jobs/index/index.js16
-rw-r--r--app/assets/javascripts/pages/projects/project.js4
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue348
-rw-r--r--app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue11
-rw-r--r--app/assets/javascripts/pipelines/components/empty_state.vue12
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue24
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue13
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue37
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue10
-rw-r--r--app/assets/javascripts/pipelines/components/header_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/nav_controls.vue18
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_url.vue43
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines.vue38
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_actions.vue27
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_artifacts.vue26
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table.vue8
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table_row.vue15
-rw-r--r--app/assets/javascripts/pipelines/components/stage.vue8
-rw-r--r--app/assets/javascripts/pipelines/components/time_ago.vue5
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines.js40
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js2
-rw-r--r--app/assets/javascripts/projects/project_new.js44
-rw-r--r--app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue2
-rw-r--r--app/assets/javascripts/registry/components/app.vue2
-rw-r--r--app/assets/javascripts/registry/components/collapsible_container.vue2
-rw-r--r--app/assets/javascripts/reports/components/issues_list.vue85
-rw-r--r--app/assets/javascripts/reports/components/report_item.vue (renamed from app/assets/javascripts/reports/components/report_issues.vue)44
-rw-r--r--app/assets/javascripts/reports/components/summary_row.vue2
-rw-r--r--app/assets/javascripts/right_sidebar.js57
-rw-r--r--app/assets/javascripts/search_autocomplete.js6
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue9
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.vue5
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue124
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/todo.vue2
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js20
-rw-r--r--app/assets/javascripts/users_select.js28
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment.vue70
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue23
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue30
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue32
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue14
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/changed_file_icon.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/clipboard_button.vue18
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue83
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue15
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue45
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue37
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue82
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_countdown.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue45
-rw-r--r--app/assets/javascripts/vue_shared/components/icon.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/loading_button.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/pagination_links.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue62
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue141
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/smart_virtual_list.vue42
-rw-r--r--app/assets/javascripts/vue_shared/components/toggle_button.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue53
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue14
-rw-r--r--app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js9
-rw-r--r--app/assets/stylesheets/framework/awards.scss5
-rw-r--r--app/assets/stylesheets/framework/blocks.scss1
-rw-r--r--app/assets/stylesheets/framework/buttons.scss42
-rw-r--r--app/assets/stylesheets/framework/common.scss7
-rw-r--r--app/assets/stylesheets/framework/contextual_sidebar.scss4
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss4
-rw-r--r--app/assets/stylesheets/framework/files.scss1
-rw-r--r--app/assets/stylesheets/framework/images.scss2
-rw-r--r--app/assets/stylesheets/framework/mixins.scss5
-rw-r--r--app/assets/stylesheets/framework/secondary_navigation_elements.scss4
-rw-r--r--app/assets/stylesheets/framework/selects.scss2
-rw-r--r--app/assets/stylesheets/framework/tables.scss6
-rw-r--r--app/assets/stylesheets/framework/timeline.scss6
-rw-r--r--app/assets/stylesheets/framework/variables.scss10
-rw-r--r--app/assets/stylesheets/pages/boards.scss197
-rw-r--r--app/assets/stylesheets/pages/builds.scss22
-rw-r--r--app/assets/stylesheets/pages/clusters.scss35
-rw-r--r--app/assets/stylesheets/pages/diff.scss50
-rw-r--r--app/assets/stylesheets/pages/environments.scss7
-rw-r--r--app/assets/stylesheets/pages/events.scss78
-rw-r--r--app/assets/stylesheets/pages/issuable.scss6
-rw-r--r--app/assets/stylesheets/pages/note_form.scss20
-rw-r--r--app/assets/stylesheets/pages/notes.scss354
-rw-r--r--app/assets/stylesheets/pages/pages.scss7
-rw-r--r--app/assets/stylesheets/pages/pipeline_schedules.scss8
-rw-r--r--app/assets/stylesheets/pages/profile.scss12
-rw-r--r--app/controllers/admin/appearances_controller.rb6
-rw-r--r--app/controllers/admin/application_settings_controller.rb2
-rw-r--r--app/controllers/chaos_controller.rb56
-rw-r--r--app/controllers/clusters/applications_controller.rb28
-rw-r--r--app/controllers/clusters/base_controller.rb37
-rw-r--r--app/controllers/clusters/clusters_controller.rb218
-rw-r--r--app/controllers/concerns/creates_commit.rb2
-rw-r--r--app/controllers/concerns/members_presentation.rb7
-rw-r--r--app/controllers/concerns/project_unauthorized.rb10
-rw-r--r--app/controllers/concerns/routable_actions.rb16
-rw-r--r--app/controllers/concerns/send_file_upload.rb4
-rw-r--r--app/controllers/groups/clusters/applications_controller.rb18
-rw-r--r--app/controllers/groups/clusters_controller.rb25
-rw-r--r--app/controllers/import/bitbucket_server_controller.rb24
-rw-r--r--app/controllers/import/gitea_controller.rb2
-rw-r--r--app/controllers/import/github_controller.rb2
-rw-r--r--app/controllers/oauth/authorizations_controller.rb2
-rw-r--r--app/controllers/projects/application_controller.rb3
-rw-r--r--app/controllers/projects/artifacts_controller.rb2
-rw-r--r--app/controllers/projects/autocomplete_sources_controller.rb18
-rw-r--r--app/controllers/projects/blob_controller.rb4
-rw-r--r--app/controllers/projects/clusters/applications_controller.rb26
-rw-r--r--app/controllers/projects/clusters_controller.rb222
-rw-r--r--app/controllers/projects/commit_controller.rb3
-rw-r--r--app/controllers/projects/merge_requests/creations_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb12
-rw-r--r--app/controllers/projects/merge_requests_controller.rb6
-rw-r--r--app/controllers/projects_controller.rb2
-rw-r--r--app/finders/autocomplete/users_finder.rb2
-rw-r--r--app/finders/clusters_finder.rb8
-rw-r--r--app/finders/concerns/finder_with_cross_project_access.rb2
-rw-r--r--app/finders/group_descendants_finder.rb2
-rw-r--r--app/finders/groups_finder.rb4
-rw-r--r--app/finders/issuable_finder.rb73
-rw-r--r--app/finders/issues_finder.rb2
-rw-r--r--app/finders/labels_finder.rb2
-rw-r--r--app/finders/milestones_finder.rb2
-rw-r--r--app/finders/personal_access_tokens_finder.rb2
-rw-r--r--app/finders/pipelines_finder.rb2
-rw-r--r--app/finders/projects_finder.rb2
-rw-r--r--app/finders/snippets_finder.rb211
-rw-r--r--app/helpers/application_settings_helper.rb4
-rw-r--r--app/helpers/avatars_helper.rb2
-rw-r--r--app/helpers/blob_helper.rb5
-rw-r--r--app/helpers/clusters_helper.rb5
-rw-r--r--app/helpers/compare_helper.rb2
-rw-r--r--app/helpers/events_helper.rb51
-rw-r--r--app/helpers/groups_helper.rb4
-rw-r--r--app/helpers/icons_helper.rb4
-rw-r--r--app/helpers/import_helper.rb4
-rw-r--r--app/helpers/labels_helper.rb2
-rw-r--r--app/helpers/merge_requests_helper.rb4
-rw-r--r--app/helpers/nav_helper.rb16
-rw-r--r--app/helpers/page_layout_helper.rb2
-rw-r--r--app/helpers/profiles_helper.rb14
-rw-r--r--app/helpers/projects_helper.rb9
-rw-r--r--app/helpers/tree_helper.rb20
-rw-r--r--app/helpers/user_callouts_helper.rb5
-rw-r--r--app/mailers/emails/issues.rb14
-rw-r--r--app/mailers/emails/merge_requests.rb14
-rw-r--r--app/mailers/previews/notify_preview.rb20
-rw-r--r--app/models/application_record.rb5
-rw-r--r--app/models/application_setting.rb24
-rw-r--r--app/models/blob.rb11
-rw-r--r--app/models/ci/build.rb95
-rw-r--r--app/models/ci/job_artifact.rb2
-rw-r--r--app/models/ci/pipeline.rb23
-rw-r--r--app/models/clusters/applications/knative.rb55
-rw-r--r--app/models/clusters/cluster.rb38
-rw-r--r--app/models/clusters/kubernetes_namespace.rb37
-rw-r--r--app/models/clusters/platforms/kubernetes.rb58
-rw-r--r--app/models/commit.rb2
-rw-r--r--app/models/commit_status.rb9
-rw-r--r--app/models/compare.rb11
-rw-r--r--app/models/concerns/awardable.rb18
-rw-r--r--app/models/concerns/blob_language_from_git_attributes.rb13
-rw-r--r--app/models/concerns/cacheable_attributes.rb4
-rw-r--r--app/models/concerns/deployable.rb30
-rw-r--r--app/models/concerns/each_batch.rb17
-rw-r--r--app/models/concerns/fast_destroy_all.rb2
-rw-r--r--app/models/concerns/issuable.rb2
-rw-r--r--app/models/concerns/mentionable.rb4
-rw-r--r--app/models/concerns/mentionable/reference_regexes.rb22
-rw-r--r--app/models/concerns/noteable.rb2
-rw-r--r--app/models/concerns/storage/legacy_namespace.rb2
-rw-r--r--app/models/concerns/token_authenticatable.rb7
-rw-r--r--app/models/concerns/token_authenticatable_strategies/base.rb6
-rw-r--r--app/models/concerns/with_uploads.rb2
-rw-r--r--app/models/deploy_token.rb5
-rw-r--r--app/models/deployment.rb84
-rw-r--r--app/models/diff_note.rb51
-rw-r--r--app/models/discussion_note.rb6
-rw-r--r--app/models/environment.rb5
-rw-r--r--app/models/environment_status.rb48
-rw-r--r--app/models/identity.rb8
-rw-r--r--app/models/identity/uniqueness_scopes.rb11
-rw-r--r--app/models/issue.rb3
-rw-r--r--app/models/key.rb4
-rw-r--r--app/models/label.rb2
-rw-r--r--app/models/members_preloader.rb16
-rw-r--r--app/models/merge_request.rb21
-rw-r--r--app/models/merge_request_diff.rb13
-rw-r--r--app/models/namespace.rb8
-rw-r--r--app/models/note.rb2
-rw-r--r--app/models/pool_repository.rb22
-rw-r--r--app/models/postgresql/replication_slot.rb11
-rw-r--r--app/models/project.rb26
-rw-r--r--app/models/project_import_state.rb13
-rw-r--r--app/models/project_services/bamboo_service.rb20
-rw-r--r--app/models/project_services/discord_service.rb57
-rw-r--r--app/models/project_services/drone_ci_service.rb16
-rw-r--r--app/models/project_services/issue_tracker_service.rb6
-rw-r--r--app/models/project_services/jira_service.rb2
-rw-r--r--app/models/project_services/kubernetes_service.rb11
-rw-r--r--app/models/project_services/mock_ci_service.rb14
-rw-r--r--app/models/project_services/teamcity_service.rb2
-rw-r--r--app/models/repository.rb16
-rw-r--r--app/models/service.rb1
-rw-r--r--app/models/shard.rb25
-rw-r--r--app/models/snippet.rb77
-rw-r--r--app/models/upload.rb18
-rw-r--r--app/models/user.rb45
-rw-r--r--app/models/user_preference.rb5
-rw-r--r--app/models/wiki_page.rb8
-rw-r--r--app/policies/ci/build_policy.rb7
-rw-r--r--app/policies/clusters/cluster_policy.rb6
-rw-r--r--app/policies/deployment_policy.rb9
-rw-r--r--app/policies/group_policy.rb4
-rw-r--r--app/policies/project_policy.rb2
-rw-r--r--app/presenters/blob_presenter.rb16
-rw-r--r--app/presenters/clusterable_presenter.rb58
-rw-r--r--app/presenters/clusters/cluster_presenter.rb10
-rw-r--r--app/presenters/commit_status_presenter.rb5
-rw-r--r--app/presenters/group_clusterable_presenter.rb36
-rw-r--r--app/presenters/project_clusterable_presenter.rb31
-rw-r--r--app/presenters/project_presenter.rb2
-rw-r--r--app/serializers/build_action_entity.rb7
-rw-r--r--app/serializers/deployment_entity.rb1
-rw-r--r--app/serializers/environment_status_entity.rb8
-rw-r--r--app/serializers/job_entity.rb18
-rw-r--r--app/serializers/user_preference_entity.rb4
-rw-r--r--app/services/auth/container_registry_authentication_service.rb2
-rw-r--r--app/services/boards/issues/move_service.rb4
-rw-r--r--app/services/ci/register_job_service.rb5
-rw-r--r--app/services/clusters/applications/check_installation_progress_service.rb9
-rw-r--r--app/services/clusters/applications/create_service.rb14
-rw-r--r--app/services/clusters/applications/install_service.rb10
-rw-r--r--app/services/clusters/create_service.rb26
-rw-r--r--app/services/clusters/gcp/finalize_creation_service.rb23
-rw-r--r--app/services/clusters/gcp/kubernetes.rb11
-rw-r--r--app/services/clusters/gcp/kubernetes/create_or_update_namespace_service.rb52
-rw-r--r--app/services/clusters/gcp/kubernetes/create_service_account_service.rb78
-rw-r--r--app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb13
-rw-r--r--app/services/commits/change_service.rb8
-rw-r--r--app/services/commits/commit_patch_service.rb61
-rw-r--r--app/services/commits/create_service.rb7
-rw-r--r--app/services/create_deployment_service.rb74
-rw-r--r--app/services/issuable/clone/attributes_rewriter.rb62
-rw-r--r--app/services/issuable/clone/base_service.rb60
-rw-r--r--app/services/issuable/clone/content_rewriter.rb65
-rw-r--r--app/services/issuable_base_service.rb12
-rw-r--r--app/services/issues/move_service.rb157
-rw-r--r--app/services/issues/update_service.rb14
-rw-r--r--app/services/keys/destroy_service.rb2
-rw-r--r--app/services/members/base_service.rb2
-rw-r--r--app/services/merge_requests/build_service.rb6
-rw-r--r--app/services/merge_requests/get_urls_service.rb4
-rw-r--r--app/services/merge_requests/refresh_service.rb23
-rw-r--r--app/services/merge_requests/reload_diffs_service.rb9
-rw-r--r--app/services/merge_requests/update_service.rb14
-rw-r--r--app/services/milestones/destroy_service.rb2
-rw-r--r--app/services/notes/base_service.rb13
-rw-r--r--app/services/notes/create_service.rb3
-rw-r--r--app/services/notes/destroy_service.rb4
-rw-r--r--app/services/notification_service.rb41
-rw-r--r--app/services/projects/move_project_authorizations_service.rb2
-rw-r--r--app/services/projects/move_project_group_links_service.rb2
-rw-r--r--app/services/projects/move_project_members_service.rb2
-rw-r--r--app/services/quick_actions/interpret_service.rb8
-rw-r--r--app/services/search/group_service.rb2
-rw-r--r--app/services/submodules/update_service.rb38
-rw-r--r--app/services/update_deployment_service.rb53
-rw-r--r--app/services/users/build_service.rb8
-rw-r--r--app/uploaders/file_uploader.rb6
-rw-r--r--app/views/abuse_reports/new.html.haml12
-rw-r--r--app/views/admin/application_settings/_ci_cd.html.haml8
-rw-r--r--app/views/admin/application_settings/_email.html.haml6
-rw-r--r--app/views/admin/background_jobs/show.html.haml5
-rw-r--r--app/views/clusters/clusters/_advanced_settings.html.haml (renamed from app/views/projects/clusters/_advanced_settings.html.haml)2
-rw-r--r--app/views/clusters/clusters/_banner.html.haml (renamed from app/views/projects/clusters/_banner.html.haml)6
-rw-r--r--app/views/clusters/clusters/_buttons.html.haml4
-rw-r--r--app/views/clusters/clusters/_cluster.html.haml16
-rw-r--r--app/views/clusters/clusters/_empty_state.html.haml14
-rw-r--r--app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml (renamed from app/views/projects/clusters/_gcp_signup_offer_banner.html.haml)0
-rw-r--r--app/views/clusters/clusters/_integration_form.html.haml (renamed from app/views/projects/clusters/_integration_form.html.haml)6
-rw-r--r--app/views/clusters/clusters/_sidebar.html.haml6
-rw-r--r--app/views/clusters/clusters/gcp/_form.html.haml (renamed from app/views/projects/clusters/gcp/_form.html.haml)6
-rw-r--r--app/views/clusters/clusters/gcp/_header.html.haml (renamed from app/views/projects/clusters/gcp/_header.html.haml)0
-rw-r--r--app/views/clusters/clusters/gcp/_show.html.haml (renamed from app/views/projects/clusters/gcp/_show.html.haml)11
-rw-r--r--app/views/clusters/clusters/index.html.haml (renamed from app/views/projects/clusters/index.html.haml)7
-rw-r--r--app/views/clusters/clusters/new.html.haml (renamed from app/views/projects/clusters/new.html.haml)8
-rw-r--r--app/views/clusters/clusters/show.html.haml (renamed from app/views/projects/clusters/show.html.haml)23
-rw-r--r--app/views/clusters/clusters/user/_form.html.haml (renamed from app/views/projects/clusters/user/_form.html.haml)13
-rw-r--r--app/views/clusters/clusters/user/_header.html.haml (renamed from app/views/projects/clusters/user/_header.html.haml)0
-rw-r--r--app/views/clusters/clusters/user/_show.html.haml (renamed from app/views/projects/clusters/user/_show.html.haml)11
-rw-r--r--app/views/dashboard/snippets/index.html.haml5
-rw-r--r--app/views/discussions/_discussion.html.haml8
-rw-r--r--app/views/events/_event.html.haml2
-rw-r--r--app/views/events/event/_common.html.haml27
-rw-r--r--app/views/events/event/_created_project.html.haml8
-rw-r--r--app/views/events/event/_note.html.haml10
-rw-r--r--app/views/events/event/_private.html.haml13
-rw-r--r--app/views/events/event/_push.html.haml10
-rw-r--r--app/views/groups/labels/index.html.haml2
-rw-r--r--app/views/groups/new.html.haml33
-rw-r--r--app/views/import/bitbucket_server/status.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml2
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml15
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml7
-rw-r--r--app/views/notify/changed_milestone_issue_email.html.haml3
-rw-r--r--app/views/notify/changed_milestone_issue_email.text.erb1
-rw-r--r--app/views/notify/changed_milestone_merge_request_email.html.haml3
-rw-r--r--app/views/notify/changed_milestone_merge_request_email.text.erb1
-rw-r--r--app/views/notify/removed_milestone_issue_email.html.haml2
-rw-r--r--app/views/notify/removed_milestone_issue_email.text.erb1
-rw-r--r--app/views/notify/removed_milestone_merge_request_email.html.haml2
-rw-r--r--app/views/notify/removed_milestone_merge_request_email.text.erb1
-rw-r--r--app/views/profiles/preferences/show.html.haml4
-rw-r--r--app/views/profiles/show.html.haml5
-rw-r--r--app/views/projects/blob/viewers/_highlight_embed.html.haml4
-rw-r--r--app/views/projects/blob/viewers/_text.html.haml2
-rw-r--r--app/views/projects/ci/builds/_build.html.haml6
-rw-r--r--app/views/projects/clusters/_cluster.html.haml24
-rw-r--r--app/views/projects/clusters/_empty_state.html.haml12
-rw-r--r--app/views/projects/clusters/_sidebar.html.haml9
-rw-r--r--app/views/projects/deployments/_rollback.haml2
-rw-r--r--app/views/projects/diffs/_text_file.html.haml2
-rw-r--r--app/views/projects/edit.html.haml4
-rw-r--r--app/views/projects/empty.html.haml2
-rw-r--r--app/views/projects/labels/index.html.haml2
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml2
-rw-r--r--app/views/projects/merge_requests/show.html.haml5
-rw-r--r--app/views/projects/notes/_more_actions_dropdown.html.haml5
-rw-r--r--app/views/projects/pages/_https_only.html.haml10
-rw-r--r--app/views/projects/pages/_list.html.haml8
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml18
-rw-r--r--app/views/projects/settings/ci_cd/_badge.html.haml4
-rw-r--r--app/views/projects/settings/ci_cd/_form.html.haml2
-rw-r--r--app/views/projects/show.html.haml2
-rw-r--r--app/views/projects/tree/_blob_item.html.haml12
-rw-r--r--app/views/projects/tree/_spinner.html.haml3
-rw-r--r--app/views/projects/tree/_submodule_item.html.haml6
-rw-r--r--app/views/projects/tree/_tree_item.html.haml9
-rw-r--r--app/views/projects/tree/_tree_row.html.haml33
-rw-r--r--app/views/search/results/_snippet_blob.html.haml2
-rw-r--r--app/views/shared/_file_highlight.html.haml6
-rw-r--r--app/views/shared/_group_form.html.haml45
-rw-r--r--app/views/shared/_label.html.haml45
-rw-r--r--app/views/shared/boards/_show.html.haml2
-rw-r--r--app/views/shared/boards/components/sidebar/_labels.html.haml4
-rw-r--r--app/views/shared/empty_states/_labels.html.haml9
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml11
-rw-r--r--app/views/shared/members/_member.html.haml2
-rw-r--r--app/views/shared/notes/_note.html.haml2
-rw-r--r--app/views/shared/notes/_notes_with_form.html.haml4
-rw-r--r--app/views/shared/projects/_search_form.html.haml2
-rw-r--r--app/views/sherlock/queries/_general.html.haml2
-rw-r--r--app/views/sherlock/transactions/_queries.html.haml2
-rw-r--r--app/views/users/_overview.html.haml6
-rw-r--r--app/workers/all_queues.yml3
-rw-r--r--app/workers/build_finished_worker.rb2
-rw-r--r--app/workers/build_success_worker.rb16
-rw-r--r--app/workers/cluster_platform_configure_worker.rb22
-rw-r--r--app/workers/cluster_provision_worker.rb2
-rw-r--r--app/workers/deployments/success_worker.rb17
-rw-r--r--app/workers/email_receiver_worker.rb2
-rw-r--r--app/workers/gitlab/github_import/advance_stage_worker.rb2
-rw-r--r--app/workers/process_commit_worker.rb2
-rw-r--r--app/workers/stuck_import_jobs_worker.rb46
-rwxr-xr-xbin/rails2
-rwxr-xr-xbin/rake2
-rwxr-xr-xbin/rspec2
-rwxr-xr-xbin/setup2
-rw-r--r--changelogs/unreleased/18933-render-index-as-readme.yml5
-rw-r--r--changelogs/unreleased/21480-parallel-job-keyword-mvc.yml5
-rw-r--r--changelogs/unreleased/22717-single-letter-identifier-external-issue-tracker.yml5
-rw-r--r--changelogs/unreleased/25140-disable-stop-button.yml5
-rw-r--r--changelogs/unreleased/28249-add-pagination.yml5
-rw-r--r--changelogs/unreleased/34758-group-cluster-controller.yml5
-rw-r--r--changelogs/unreleased/41545-gitlab-merge-request-status-could-not-connect-to-the-ci-server-please-check-your-settings-and-try-again.yml5
-rw-r--r--changelogs/unreleased/43521-keep-personal-emails-private.yml5
-rw-r--r--changelogs/unreleased/44012-filter-reactions-none-any.yml5
-rw-r--r--changelogs/unreleased/47008-issue-board-card-design.yml5
-rw-r--r--changelogs/unreleased/48475-gitlab-pages-settings-regressions.yml5
-rw-r--r--changelogs/unreleased/49403-redesign-activity-feed.yml4
-rw-r--r--changelogs/unreleased/50962-create-new-group-rename-form-fields-and-update-ui.yml5
-rw-r--r--changelogs/unreleased/51259-ci-cd-gitlab-ui-1.yml5
-rw-r--r--changelogs/unreleased/51259-ci-cd-gitlab-ui.yml5
-rw-r--r--changelogs/unreleased/51259-ci-cd-tooltips.yml6
-rw-r--r--changelogs/unreleased/51620-cannot-add-label-to-issue-from-board.yml4
-rw-r--r--changelogs/unreleased/51716-add-kubernetes-namespace-background-migration.yml5
-rw-r--r--changelogs/unreleased/51716-create-kube-namespace.yml5
-rw-r--r--changelogs/unreleased/52300-pool-repositories.yml5
-rw-r--r--changelogs/unreleased/52382-filter-milestone-api-none-any.yml5
-rw-r--r--changelogs/unreleased/52548-links-in-tabs-of-the-labels-index-pages-ends-with-html.yml5
-rw-r--r--changelogs/unreleased/52767-more-chaos-for-gitlab.yml5
-rw-r--r--changelogs/unreleased/52771-ldap-users-can-t-choose-private-or-internal-when-creating-a-new-group.yml5
-rw-r--r--changelogs/unreleased/52925-scheduled-pipelines-ui-problems.yml5
-rw-r--r--changelogs/unreleased/52940-fix-internal-email-pattern-not-respected.yml5
-rw-r--r--changelogs/unreleased/53052-mg-fix-broken-ie11.yml5
-rw-r--r--changelogs/unreleased/53070-fix-enable-usage-ping-link.yml5
-rw-r--r--changelogs/unreleased/53227-empty-list.yml6
-rw-r--r--changelogs/unreleased/53230-remove_personal_access_tokens_finder_find_by_method.yml5
-rw-r--r--changelogs/unreleased/53289-update-haml_lint-to-0-28-0.yml5
-rw-r--r--changelogs/unreleased/53291-update-ffaker-to-2-10-0.yml5
-rw-r--r--changelogs/unreleased/53362-allow-concurrency-in-puma.yml5
-rw-r--r--changelogs/unreleased/53450-wrong-value-for-kubernetes_version-variable.yml5
-rw-r--r--changelogs/unreleased/53533-fix-broken-link.yml5
-rw-r--r--changelogs/unreleased/53535-sticky-archived.yml5
-rw-r--r--changelogs/unreleased/53626-update-config-map-on-install-retry.yml5
-rw-r--r--changelogs/unreleased/53636-fix-rendering-of-any-user-filter.yml5
-rw-r--r--changelogs/unreleased/53640-follow-up-from-resolve-redesign-activity-feed.yml4
-rw-r--r--changelogs/unreleased/53816-empty-label-menu-if-not-logged-in.yml5
-rw-r--r--changelogs/unreleased/53879-kube-token-nil.yml5
-rw-r--r--changelogs/unreleased/53888-missing-favicon.yml5
-rw-r--r--changelogs/unreleased/53972-fix-fill-shards.yml5
-rw-r--r--changelogs/unreleased/54002-activity-feed-missing-padding-in-event-note-when-a-branch-is-deleted.yml5
-rw-r--r--changelogs/unreleased/6500-fix-misaligned-approvers-dropdown.yml5
-rw-r--r--changelogs/unreleased/7737-ci-pipeline-view-slowed-down-massivly-if-security-tabs-has-many-entries-ee.yml5
-rw-r--r--changelogs/unreleased/ab-45608-stuck-mr-query.yml5
-rw-r--r--changelogs/unreleased/add-action-to-deployment.yml5
-rw-r--r--changelogs/unreleased/add-scheduled-flag-to-job-entity.yml5
-rw-r--r--changelogs/unreleased/ashmckenzie-hmac-token-decode-and-tests.yml5
-rw-r--r--changelogs/unreleased/auto_devops_kubernetes_active.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-add-discord-service.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-update-push-new-merge-request-url.yml5
-rw-r--r--changelogs/unreleased/bvl-patches-via-mail.yml5
-rw-r--r--changelogs/unreleased/ccr-51052_keep_labels_on_issue.yml5
-rw-r--r--changelogs/unreleased/ccr-51520_change_milestone_email.yml5
-rw-r--r--changelogs/unreleased/diff-expand-all-button.yml5
-rw-r--r--changelogs/unreleased/disallow-retry-of-old-builds.yml5
-rw-r--r--changelogs/unreleased/discussion-perf-improvement.yml5
-rw-r--r--changelogs/unreleased/dm-api-merge-requests-index-merged-at.yml5
-rw-r--r--changelogs/unreleased/dm-commit-email-select-options.yml5
-rw-r--r--changelogs/unreleased/drop-gcp-cluster-table.yml5
-rw-r--r--changelogs/unreleased/fast_project_blob_path.yml5
-rw-r--r--changelogs/unreleased/fix-deployment-metrics-in-mr-widget.yml6
-rw-r--r--changelogs/unreleased/fix-error-handling-bugs-in-kubernetes-integration.yml5
-rw-r--r--changelogs/unreleased/fix-stuck-import-jobs-query-performance-issue.yml5
-rw-r--r--changelogs/unreleased/fix-tags-for-envs.yml5
-rw-r--r--changelogs/unreleased/fj-41213-api-update-submodule-commit.yml5
-rw-r--r--changelogs/unreleased/fj-50890-fix-commit-message-wiki-new-page.yml5
-rw-r--r--changelogs/unreleased/fj-bump-gitaly-0-129-0.yml5
-rw-r--r--changelogs/unreleased/fl-missing-i18n.yml5
-rw-r--r--changelogs/unreleased/frozen-string-enable-lib-gitlab-ci-remain.yml5
-rw-r--r--changelogs/unreleased/frozen-string-enable-lib-gitlab-ci.yml5
-rw-r--r--changelogs/unreleased/frozen-string-lib-gitlab-more.yml5
-rw-r--r--changelogs/unreleased/gl-ui-loading-icon.yml5
-rw-r--r--changelogs/unreleased/gl-ui-modal.yml5
-rw-r--r--changelogs/unreleased/gl-ui-pagination.yml5
-rw-r--r--changelogs/unreleased/gl-ui-progress-bar.yml5
-rw-r--r--changelogs/unreleased/gl-ui-tooltip.yml5
-rw-r--r--changelogs/unreleased/gt-align-sign-in-button.yml5
-rw-r--r--changelogs/unreleased/gt-fix-ide-typos-in-props.yml5
-rw-r--r--changelogs/unreleased/gt-fix-quick-links-button-styles.yml5
-rw-r--r--changelogs/unreleased/gt-update-project-and-group-labels-empty-state.yml5
-rw-r--r--changelogs/unreleased/gt-use-merge-request-prefix-in-event-feed-title.yml5
-rw-r--r--changelogs/unreleased/ignore-environment-validation-failure.yml5
-rw-r--r--changelogs/unreleased/introduce-knative-support.yml5
-rw-r--r--changelogs/unreleased/issue_51323.yml5
-rw-r--r--changelogs/unreleased/kinolaev-master-patch-91872.yml5
-rw-r--r--changelogs/unreleased/kubernetes-http-response-code.yml5
-rw-r--r--changelogs/unreleased/max_retries_when.yml5
-rw-r--r--changelogs/unreleased/mr-file-tree-inline-fluid-width-fix.yml5
-rw-r--r--changelogs/unreleased/mr-image-commenting.yml5
-rw-r--r--changelogs/unreleased/mr-tree-filter-path-name.yml5
-rw-r--r--changelogs/unreleased/osw-comment-on-any-line-on-diffs.yml5
-rw-r--r--changelogs/unreleased/osw-fallback-on-blank-refs.yml5
-rw-r--r--changelogs/unreleased/rails5-mysql-milliseconds-deployment-spec.yml5
-rw-r--r--changelogs/unreleased/rake-gitaly-check.yml5
-rw-r--r--changelogs/unreleased/refactor-snippets-finder.yml5
-rw-r--r--changelogs/unreleased/related_mrs.yml5
-rw-r--r--changelogs/unreleased/remove-asset-sync.yml5
-rw-r--r--changelogs/unreleased/remove-ci_enable_scheduled_build-feature-flag.yml5
-rw-r--r--changelogs/unreleased/remove-duplicate-primary-button-in-dashboard-snippets.yml5
-rw-r--r--changelogs/unreleased/remove-experimental-label-from-cluster-views.yml5
-rw-r--r--changelogs/unreleased/replace-tooltip-in-markdown-component.yml5
-rw-r--r--changelogs/unreleased/rs-cherry-pick-api.yml5
-rw-r--r--changelogs/unreleased/rs-revert-api.yml5
-rw-r--r--changelogs/unreleased/scheduled-manual-jobs-environment-play-buttons.yml5
-rw-r--r--changelogs/unreleased/security-kubeclient-ssrf.yml5
-rw-r--r--changelogs/unreleased/sh-53180-append-path.yml5
-rw-r--r--changelogs/unreleased/sh-bump-ruby-2-5-3.yml5
-rw-r--r--changelogs/unreleased/sh-fix-issue-52176.yml5
-rw-r--r--changelogs/unreleased/sh-fix-issue-52649.yml5
-rw-r--r--changelogs/unreleased/sh-fix-search-relative-urls.yml5
-rw-r--r--changelogs/unreleased/sh-optimize-merge-request-project-lookup.yml5
-rw-r--r--changelogs/unreleased/sh-optimize-mr-commit-sha-lookup.yml5
-rw-r--r--changelogs/unreleased/sh-optimize-reload-diffs-service.yml5
-rw-r--r--changelogs/unreleased/sh-paginate-bitbucket-server-imports.yml5
-rw-r--r--changelogs/unreleased/stateful_deployments.yml5
-rw-r--r--changelogs/unreleased/switch-rails.yml5
-rw-r--r--changelogs/unreleased/tc-index-uploads-file-store.yml5
-rw-r--r--changelogs/unreleased/toggle-sidebar-alignment.yml5
-rw-r--r--changelogs/unreleased/top_level_clusters_controller.yml6
-rw-r--r--changelogs/unreleased/triggermesh-phase2-serverless.yml5
-rw-r--r--changelogs/unreleased/update_license_management_job.yml5
-rw-r--r--changelogs/unreleased/upgrade-workhorse-7-1-0.yml5
-rw-r--r--changelogs/unreleased/winh-delayed-jobs-dynamic-timer.yml5
-rw-r--r--changelogs/unreleased/winh-job-list-dynamic-timer.yml5
-rw-r--r--changelogs/unreleased/zj-bump-gitaly-0-128.yml5
-rw-r--r--changelogs/unreleased/zj-remove-broken-storage.yml5
-rw-r--r--config/application.rb4
-rw-r--r--config/boot.rb4
-rw-r--r--config/dependency_decisions.yml2
-rw-r--r--config/environment.rb6
-rw-r--r--config/environments/development.rb2
-rw-r--r--config/environments/production.rb2
-rw-r--r--config/gitlab.yml.example6
-rw-r--r--config/initializers/8_metrics.rb6
-rw-r--r--config/initializers/asset_sync.rb31
-rw-r--r--config/initializers/fill_shards.rb3
-rw-r--r--config/initializers/kubeclient.rb21
-rw-r--r--config/initializers/mysql_set_length_for_binary_indexes.rb50
-rw-r--r--config/locales/en.yml2
-rw-r--r--config/routes.rb24
-rw-r--r--config/routes/group.rb2
-rw-r--r--config/routes/project.rb20
-rw-r--r--config/sidekiq_queues.yml1
-rw-r--r--danger/documentation/Dangerfile29
-rw-r--r--danger/metadata/Dangerfile7
-rw-r--r--db/fixtures/development/09_issues.rb3
-rw-r--r--db/fixtures/development/17_cycle_analytics.rb10
-rw-r--r--db/migrate/20180413022611_create_missing_namespace_for_internal_users.rb4
-rw-r--r--db/migrate/20180912111628_add_knative_application.rb20
-rw-r--r--db/migrate/20180927073410_add_index_to_project_deploy_tokens_deploy_token_id.rb18
-rw-r--r--db/migrate/20181005125926_add_index_to_uploads_store.rb17
-rw-r--r--db/migrate/20181015155839_add_finished_at_to_deployments.rb15
-rw-r--r--db/migrate/20181016141739_add_status_to_deployments.rb29
-rw-r--r--db/migrate/20181019032400_add_shards_table.rb11
-rw-r--r--db/migrate/20181019032408_add_repositories_table.rb15
-rw-r--r--db/migrate/20181019105553_add_projects_pool_repository_id_foreign_key.rb22
-rw-r--r--db/migrate/20181022135539_add_index_on_status_to_deployments.rb19
-rw-r--r--db/migrate/20181023104858_add_archive_builds_duration_to_application_settings.rb11
-rw-r--r--db/migrate/20181023144439_add_partial_index_for_legacy_successful_deployments.rb18
-rw-r--r--db/migrate/20181025115728_add_private_commit_email_hostname_to_application_settings.rb11
-rw-r--r--db/migrate/20181026143227_migrate_snippets_access_level_default_value.rb42
-rw-r--r--db/migrate/20181031190558_drop_fk_gcp_clusters_table.rb37
-rw-r--r--db/migrate/20181031190559_drop_gcp_clusters_table.rb53
-rw-r--r--db/migrate/20181101144347_add_index_for_stuck_mr_query.rb16
-rw-r--r--db/migrate/20181106135939_add_index_to_deployments.rb17
-rw-r--r--db/post_migrate/20170717111152_cleanup_move_system_upload_folder_symlink.rb2
-rw-r--r--db/post_migrate/20181022173835_enqueue_populate_cluster_kubernetes_namespace.rb18
-rw-r--r--db/post_migrate/20181030135124_fill_empty_finished_at_in_deployments.rb27
-rw-r--r--db/post_migrate/20181105201455_steal_fill_store_upload.rb31
-rw-r--r--db/post_migrate/20181107054254_remove_restricted_todos_again.rb32
-rw-r--r--db/schema.rb979
-rw-r--r--doc/administration/auth/authentiq.md36
-rw-r--r--doc/administration/auth/okta.md2
-rw-r--r--doc/administration/container_registry.md5
-rw-r--r--doc/administration/custom_hooks.md2
-rw-r--r--doc/administration/git_protocol.md4
-rw-r--r--doc/administration/high_availability/nfs.md10
-rw-r--r--doc/administration/high_availability/redis.md2
-rw-r--r--doc/administration/logs.md12
-rw-r--r--doc/administration/monitoring/performance/influxdb_configuration.md6
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md1
-rw-r--r--doc/administration/monitoring/prometheus/index.md121
-rw-r--r--doc/administration/operations/fast_ssh_key_lookup.md16
-rw-r--r--doc/administration/raketasks/maintenance.md3
-rw-r--r--doc/administration/reply_by_email_postfix_setup.md2
-rw-r--r--doc/administration/troubleshooting/debug.md6
-rw-r--r--doc/api/README.md3
-rw-r--r--doc/api/commits.md42
-rw-r--r--doc/api/events.md4
-rw-r--r--doc/api/issues.md99
-rw-r--r--doc/api/merge_requests.md89
-rw-r--r--doc/api/repositories.md1
-rw-r--r--doc/api/repository_submodules.md49
-rw-r--r--doc/api/services.md36
-rw-r--r--doc/ci/autodeploy/quick_start_guide.md6
-rw-r--r--doc/ci/docker/using_docker_build.md8
-rw-r--r--doc/ci/examples/artifactory_and_gitlab/index.md4
-rw-r--r--doc/ci/examples/browser_performance.md73
-rw-r--r--doc/ci/examples/code_quality.md82
-rw-r--r--doc/ci/examples/container_scanning.md83
-rw-r--r--doc/ci/examples/dast.md67
-rw-r--r--doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/cloud_foundry_secret_variables.pngbin49735 -> 0 bytes
-rw-r--r--doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/cloud_foundry_variables.pngbin0 -> 28170 bytes
-rw-r--r--doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md4
-rw-r--r--doc/ci/examples/deployment/README.md8
-rw-r--r--doc/ci/examples/deployment/composer-npm-deploy.md26
-rw-r--r--doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md2
-rw-r--r--doc/ci/examples/laravel_with_gitlab_and_envoy/img/secret_variables_page.pngbin96864 -> 0 bytes
-rw-r--r--doc/ci/examples/laravel_with_gitlab_and_envoy/img/variables_page.pngbin0 -> 27538 bytes
-rw-r--r--doc/ci/examples/laravel_with_gitlab_and_envoy/index.md10
-rw-r--r--doc/ci/examples/test-and-deploy-python-application-to-heroku.md12
-rw-r--r--doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md11
-rw-r--r--doc/ci/quick_start/README.md2
-rw-r--r--doc/ci/runners/README.md6
-rw-r--r--doc/ci/services/mysql.md2
-rw-r--r--doc/ci/variables/README.md2
-rw-r--r--doc/ci/yaml/README.md161
-rw-r--r--doc/development/README.md2
-rw-r--r--doc/development/adding_database_indexes.md6
-rw-r--r--doc/development/api_graphql_styleguide.md2
-rw-r--r--doc/development/build_test_package.md13
-rw-r--r--doc/development/chaos_endpoints.md117
-rw-r--r--doc/development/code_review.md70
-rw-r--r--doc/development/contributing/issue_workflow.md4
-rw-r--r--doc/development/contributing/merge_request_workflow.md3
-rw-r--r--doc/development/diffs.md24
-rw-r--r--doc/development/documentation/index.md550
-rw-r--r--doc/development/documentation/styleguide.md279
-rw-r--r--doc/development/documentation/workflow.md33
-rw-r--r--doc/development/ee_features.md153
-rw-r--r--doc/development/fe_guide/icons.md4
-rw-r--r--doc/development/fe_guide/style_guide_scss.md6
-rw-r--r--doc/development/fe_guide/vue.md8
-rw-r--r--doc/development/fe_guide/vuex.md12
-rw-r--r--doc/development/github_importer.md18
-rw-r--r--doc/development/i18n/proofreader.md1
-rw-r--r--doc/development/instrumentation.md8
-rw-r--r--doc/development/logging.md144
-rw-r--r--doc/development/new_fe_guide/development/performance.md2
-rw-r--r--doc/development/new_fe_guide/development/testing.md295
-rw-r--r--doc/development/performance.md25
-rw-r--r--doc/development/post_deployment_migrations.md6
-rw-r--r--doc/development/query_count_limits.md4
-rw-r--r--doc/development/rolling_out_changes_using_feature_flags.md33
-rw-r--r--doc/development/swapping_tables.md4
-rw-r--r--doc/development/switching_to_rails5.md27
-rw-r--r--doc/development/testing_guide/end_to_end_tests.md69
-rw-r--r--doc/development/testing_guide/review_apps.md92
-rw-r--r--doc/development/testing_guide/smoke.md9
-rw-r--r--doc/development/testing_guide/testing_levels.md93
-rw-r--r--doc/development/ui_guide.md10
-rw-r--r--doc/development/utilities.md4
-rw-r--r--doc/development/ux_guide/tips.md18
-rw-r--r--doc/development/ux_guide/users.md16
-rw-r--r--doc/development/what_requires_downtime.md2
-rw-r--r--doc/gitlab-basics/create-your-ssh-keys.md2
-rw-r--r--doc/install/README.md6
-rw-r--r--doc/install/aws/img/add_tags.pngbin0 -> 17834 bytes
-rw-r--r--doc/install/aws/img/associate_subnet_gateway.pngbin0 -> 16522 bytes
-rw-r--r--doc/install/aws/img/associate_subnet_gateway_2.pngbin0 -> 10617 bytes
-rw-r--r--doc/install/aws/img/aws_diagram.pngbin0 -> 502497 bytes
-rw-r--r--doc/install/aws/img/choose_ami.pngbin0 -> 4892 bytes
-rw-r--r--doc/install/aws/img/create_gateway.pngbin0 -> 13927 bytes
-rw-r--r--doc/install/aws/img/create_route_table.pngbin0 -> 8293 bytes
-rw-r--r--doc/install/aws/img/create_security_group.pngbin0 -> 12594 bytes
-rw-r--r--doc/install/aws/img/create_subnet.pngbin0 -> 16679 bytes
-rw-r--r--doc/install/aws/img/create_vpc.pngbin0 -> 15613 bytes
-rw-r--r--doc/install/aws/img/ec_az.pngbin0 -> 10476 bytes
-rw-r--r--doc/install/aws/img/ec_subnet.pngbin0 -> 23517 bytes
-rw-r--r--doc/install/aws/img/policies.pngbin0 -> 39723 bytes
-rw-r--r--doc/install/aws/img/rds_subnet_group.pngbin0 -> 30107 bytes
-rw-r--r--doc/install/aws/index.md655
-rw-r--r--doc/install/database_mysql.md48
-rw-r--r--doc/install/digitaloceandocker.md102
-rw-r--r--doc/install/installation.md12
-rw-r--r--doc/install/kubernetes/gitlab_chart.md2
-rw-r--r--doc/install/kubernetes/gitlab_runner_chart.md4
-rw-r--r--doc/install/openshift_and_gitlab/index.md2
-rw-r--r--doc/install/requirements.md2
-rw-r--r--doc/integration/akismet.md16
-rw-r--r--doc/integration/auth0.md2
-rw-r--r--doc/integration/bitbucket.md1
-rw-r--r--doc/integration/google.md2
-rw-r--r--doc/integration/recaptcha.md20
-rw-r--r--doc/raketasks/backup_restore.md315
-rw-r--r--doc/raketasks/import.md72
-rw-r--r--doc/security/rack_attack.md18
-rw-r--r--doc/security/two_factor_authentication.md4
-rw-r--r--doc/ssh/README.md243
-rw-r--r--doc/topics/authentication/index.md2
-rw-r--r--doc/topics/autodevops/index.md8
-rw-r--r--doc/topics/git/troubleshooting_git.md15
-rw-r--r--doc/university/README.md11
-rw-r--r--doc/university/training/end-user/README.md71
-rw-r--r--doc/university/training/topics/additional_resources.md12
-rw-r--r--doc/university/training/topics/bisect.md12
-rw-r--r--doc/university/training/topics/getting_started.md9
-rw-r--r--doc/university/training/topics/git_log.md10
-rw-r--r--doc/university/training/topics/merge_conflicts.md18
-rw-r--r--doc/university/training/topics/rollback_commits.md18
-rw-r--r--doc/university/training/topics/stash.md12
-rw-r--r--doc/university/training/topics/tags.md2
-rw-r--r--doc/university/training/user_training.md267
-rw-r--r--doc/update/11.4-to-11.5.md28
-rw-r--r--doc/update/README.md8
-rw-r--r--doc/user/admin_area/settings/continuous_integration.md16
-rw-r--r--doc/user/admin_area/settings/email.md17
-rw-r--r--doc/user/admin_area/settings/usage_statistics.md6
-rw-r--r--doc/user/markdown.md137
-rw-r--r--doc/user/permissions.md5
-rw-r--r--doc/user/profile/account/two_factor_authentication.md4
-rw-r--r--doc/user/profile/index.md46
-rw-r--r--doc/user/project/clusters/eks_and_gitlab/img/rbac.pngbin0 -> 49712 bytes
-rw-r--r--doc/user/project/clusters/eks_and_gitlab/index.md120
-rw-r--r--doc/user/project/clusters/index.md117
-rw-r--r--doc/user/project/container_registry.md4
-rw-r--r--doc/user/project/deploy_tokens/index.md12
-rw-r--r--doc/user/project/import/clearcase.md2
-rw-r--r--doc/user/project/import/github.md30
-rw-r--r--doc/user/project/import/index.md3
-rw-r--r--doc/user/project/import/manifest.md10
-rw-r--r--doc/user/project/integrations/bugzilla.md5
-rw-r--r--doc/user/project/integrations/discord_notifications.md29
-rw-r--r--doc/user/project/integrations/jira_cloud_configuration.md5
-rw-r--r--doc/user/project/integrations/jira_server_configuration.md12
-rw-r--r--doc/user/project/integrations/mattermost.md6
-rw-r--r--doc/user/project/integrations/project_services.md1
-rw-r--r--doc/user/project/integrations/redmine.md7
-rw-r--r--doc/user/project/integrations/webhooks.md8
-rw-r--r--doc/user/project/issues/automatic_issue_closing.md9
-rw-r--r--doc/user/project/merge_requests/index.md46
-rw-r--r--doc/user/project/new_ci_build_permissions_model.md2
-rw-r--r--doc/user/project/repository/index.md42
-rw-r--r--doc/workflow/notifications.md45
-rw-r--r--doc/workflow/shortcuts.md2
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/commits.rb42
-rw-r--r--lib/api/entities.rb32
-rw-r--r--lib/api/issues.rb43
-rw-r--r--lib/api/merge_requests.rb2
-rw-r--r--lib/api/services.rb9
-rw-r--r--lib/api/snippets.rb2
-rw-r--r--lib/api/submodules.rb47
-rw-r--r--lib/api/users.rb4
-rw-r--r--lib/banzai/filter/absolute_link_filter.rb1
-rw-r--r--lib/banzai/filter/issuable_state_filter.rb11
-rw-r--r--lib/banzai/issuable_extractor.rb41
-rw-r--r--lib/bitbucket_server/client.rb8
-rw-r--r--lib/bitbucket_server/collection.rb24
-rw-r--r--lib/bitbucket_server/connection.rb22
-rw-r--r--lib/bitbucket_server/paginator.rb27
-rw-r--r--lib/extracts_path.rb2
-rw-r--r--lib/gitlab/auth/o_auth/auth_hash.rb2
-rw-r--r--lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table.rb82
-rw-r--r--lib/gitlab/background_migration/redact_links.rb23
-rw-r--r--lib/gitlab/background_migration/redact_links/redactable.rb21
-rw-r--r--lib/gitlab/background_migration/remove_restricted_todos.rb2
-rw-r--r--lib/gitlab/background_migration/set_confidential_note_events_on_services.rb4
-rw-r--r--lib/gitlab/background_migration/set_confidential_note_events_on_webhooks.rb4
-rw-r--r--lib/gitlab/blame.rb3
-rw-r--r--lib/gitlab/ci/ansi2html.rb4
-rw-r--r--lib/gitlab/ci/build/artifacts/adapters/gzip_stream.rb2
-rw-r--r--lib/gitlab/ci/build/artifacts/adapters/raw_stream.rb2
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata.rb2
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata/entry.rb2
-rw-r--r--lib/gitlab/ci/build/artifacts/path.rb2
-rw-r--r--lib/gitlab/ci/build/credentials/base.rb2
-rw-r--r--lib/gitlab/ci/build/credentials/factory.rb2
-rw-r--r--lib/gitlab/ci/build/credentials/registry.rb2
-rw-r--r--lib/gitlab/ci/build/image.rb2
-rw-r--r--lib/gitlab/ci/build/policy.rb2
-rw-r--r--lib/gitlab/ci/build/policy/kubernetes.rb2
-rw-r--r--lib/gitlab/ci/build/policy/refs.rb2
-rw-r--r--lib/gitlab/ci/build/policy/specification.rb2
-rw-r--r--lib/gitlab/ci/build/policy/variables.rb2
-rw-r--r--lib/gitlab/ci/build/step.rb2
-rw-r--r--lib/gitlab/ci/charts.rb2
-rw-r--r--lib/gitlab/ci/config.rb2
-rw-r--r--lib/gitlab/ci/config/entry/artifacts.rb2
-rw-r--r--lib/gitlab/ci/config/entry/attributable.rb2
-rw-r--r--lib/gitlab/ci/config/entry/boolean.rb2
-rw-r--r--lib/gitlab/ci/config/entry/cache.rb2
-rw-r--r--lib/gitlab/ci/config/entry/commands.rb2
-rw-r--r--lib/gitlab/ci/config/entry/configurable.rb2
-rw-r--r--lib/gitlab/ci/config/entry/coverage.rb2
-rw-r--r--lib/gitlab/ci/config/entry/environment.rb2
-rw-r--r--lib/gitlab/ci/config/entry/factory.rb2
-rw-r--r--lib/gitlab/ci/config/entry/global.rb2
-rw-r--r--lib/gitlab/ci/config/entry/hidden.rb2
-rw-r--r--lib/gitlab/ci/config/entry/image.rb2
-rw-r--r--lib/gitlab/ci/config/entry/job.rb26
-rw-r--r--lib/gitlab/ci/config/entry/jobs.rb2
-rw-r--r--lib/gitlab/ci/config/entry/key.rb2
-rw-r--r--lib/gitlab/ci/config/entry/legacy_validation_helpers.rb2
-rw-r--r--lib/gitlab/ci/config/entry/node.rb2
-rw-r--r--lib/gitlab/ci/config/entry/paths.rb2
-rw-r--r--lib/gitlab/ci/config/entry/policy.rb2
-rw-r--r--lib/gitlab/ci/config/entry/retry.rb90
-rw-r--r--lib/gitlab/ci/config/entry/script.rb2
-rw-r--r--lib/gitlab/ci/config/entry/service.rb2
-rw-r--r--lib/gitlab/ci/config/entry/services.rb2
-rw-r--r--lib/gitlab/ci/config/entry/simplifiable.rb2
-rw-r--r--lib/gitlab/ci/config/entry/stage.rb2
-rw-r--r--lib/gitlab/ci/config/entry/stages.rb2
-rw-r--r--lib/gitlab/ci/config/entry/undefined.rb2
-rw-r--r--lib/gitlab/ci/config/entry/unspecified.rb2
-rw-r--r--lib/gitlab/ci/config/entry/validatable.rb2
-rw-r--r--lib/gitlab/ci/config/entry/validator.rb2
-rw-r--r--lib/gitlab/ci/config/entry/validators.rb26
-rw-r--r--lib/gitlab/ci/config/entry/variables.rb2
-rw-r--r--lib/gitlab/ci/config/loader.rb2
-rw-r--r--lib/gitlab/ci/config/normalizer.rb65
-rw-r--r--lib/gitlab/ci/cron_parser.rb2
-rw-r--r--lib/gitlab/ci/mask_secret.rb4
-rw-r--r--lib/gitlab/ci/model.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/base.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/build.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/command.rb5
-rw-r--r--lib/gitlab/ci/pipeline/chain/create.rb19
-rw-r--r--lib/gitlab/ci/pipeline/chain/helpers.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/populate.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/sequence.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/skip.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/validate/abilities.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/validate/config.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/validate/repository.rb2
-rw-r--r--lib/gitlab/ci/pipeline/duration.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/base.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/equals.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/matches.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/null.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/operator.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/string.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/value.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/variable.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexer.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression/parser.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression/statement.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression/token.rb2
-rw-r--r--lib/gitlab/ci/pipeline/seed/base.rb2
-rw-r--r--lib/gitlab/ci/pipeline/seed/build.rb2
-rw-r--r--lib/gitlab/ci/pipeline/seed/stage.rb2
-rw-r--r--lib/gitlab/ci/reports/test_case.rb2
-rw-r--r--lib/gitlab/ci/reports/test_reports.rb2
-rw-r--r--lib/gitlab/ci/reports/test_reports_comparer.rb2
-rw-r--r--lib/gitlab/ci/reports/test_suite.rb2
-rw-r--r--lib/gitlab/ci/reports/test_suite_comparer.rb2
-rw-r--r--lib/gitlab/ci/status/build/action.rb2
-rw-r--r--lib/gitlab/ci/status/build/cancelable.rb2
-rw-r--r--lib/gitlab/ci/status/build/canceled.rb2
-rw-r--r--lib/gitlab/ci/status/build/common.rb2
-rw-r--r--lib/gitlab/ci/status/build/created.rb2
-rw-r--r--lib/gitlab/ci/status/build/erased.rb2
-rw-r--r--lib/gitlab/ci/status/build/factory.rb2
-rw-r--r--lib/gitlab/ci/status/build/failed.rb5
-rw-r--r--lib/gitlab/ci/status/build/failed_allowed.rb2
-rw-r--r--lib/gitlab/ci/status/build/manual.rb2
-rw-r--r--lib/gitlab/ci/status/build/pending.rb2
-rw-r--r--lib/gitlab/ci/status/build/play.rb2
-rw-r--r--lib/gitlab/ci/status/build/retried.rb2
-rw-r--r--lib/gitlab/ci/status/build/retryable.rb2
-rw-r--r--lib/gitlab/ci/status/build/scheduled.rb15
-rw-r--r--lib/gitlab/ci/status/build/skipped.rb2
-rw-r--r--lib/gitlab/ci/status/build/stop.rb2
-rw-r--r--lib/gitlab/ci/status/build/unschedule.rb2
-rw-r--r--lib/gitlab/ci/status/canceled.rb2
-rw-r--r--lib/gitlab/ci/status/core.rb2
-rw-r--r--lib/gitlab/ci/status/created.rb2
-rw-r--r--lib/gitlab/ci/status/extended.rb2
-rw-r--r--lib/gitlab/ci/status/external/common.rb2
-rw-r--r--lib/gitlab/ci/status/external/factory.rb2
-rw-r--r--lib/gitlab/ci/status/factory.rb2
-rw-r--r--lib/gitlab/ci/status/failed.rb2
-rw-r--r--lib/gitlab/ci/status/group/common.rb2
-rw-r--r--lib/gitlab/ci/status/group/factory.rb2
-rw-r--r--lib/gitlab/ci/status/manual.rb2
-rw-r--r--lib/gitlab/ci/status/pending.rb2
-rw-r--r--lib/gitlab/ci/status/pipeline/blocked.rb2
-rw-r--r--lib/gitlab/ci/status/pipeline/common.rb2
-rw-r--r--lib/gitlab/ci/status/pipeline/delayed.rb2
-rw-r--r--lib/gitlab/ci/status/pipeline/factory.rb2
-rw-r--r--lib/gitlab/ci/status/running.rb2
-rw-r--r--lib/gitlab/ci/status/scheduled.rb2
-rw-r--r--lib/gitlab/ci/status/skipped.rb2
-rw-r--r--lib/gitlab/ci/status/stage/common.rb2
-rw-r--r--lib/gitlab/ci/status/stage/factory.rb2
-rw-r--r--lib/gitlab/ci/status/success.rb2
-rw-r--r--lib/gitlab/ci/status/success_warning.rb2
-rw-r--r--lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml45
-rw-r--r--lib/gitlab/ci/templates/Maven.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/trace.rb2
-rw-r--r--lib/gitlab/ci/trace/chunked_io.rb15
-rw-r--r--lib/gitlab/ci/trace/section_parser.rb2
-rw-r--r--lib/gitlab/ci/trace/stream.rb5
-rw-r--r--lib/gitlab/ci/variables/collection.rb2
-rw-r--r--lib/gitlab/ci/variables/collection/item.rb2
-rw-r--r--lib/gitlab/ci/yaml_processor.rb6
-rw-r--r--lib/gitlab/cluster/puma_worker_killer_initializer.rb6
-rw-r--r--lib/gitlab/conflict/file.rb38
-rw-r--r--lib/gitlab/conflict/file_collection.rb2
-rw-r--r--lib/gitlab/cross_project_access/check_collection.rb2
-rw-r--r--lib/gitlab/cross_project_access/check_info.rb2
-rw-r--r--lib/gitlab/cross_project_access/class_methods.rb2
-rw-r--r--lib/gitlab/cycle_analytics/base_event_fetcher.rb2
-rw-r--r--lib/gitlab/cycle_analytics/base_query.rb2
-rw-r--r--lib/gitlab/cycle_analytics/base_stage.rb2
-rw-r--r--lib/gitlab/cycle_analytics/code_event_fetcher.rb2
-rw-r--r--lib/gitlab/cycle_analytics/code_stage.rb2
-rw-r--r--lib/gitlab/cycle_analytics/event_fetcher.rb2
-rw-r--r--lib/gitlab/cycle_analytics/issue_event_fetcher.rb2
-rw-r--r--lib/gitlab/cycle_analytics/issue_stage.rb2
-rw-r--r--lib/gitlab/cycle_analytics/metrics_tables.rb2
-rw-r--r--lib/gitlab/cycle_analytics/permissions.rb2
-rw-r--r--lib/gitlab/cycle_analytics/plan_event_fetcher.rb2
-rw-r--r--lib/gitlab/cycle_analytics/plan_stage.rb2
-rw-r--r--lib/gitlab/cycle_analytics/production_event_fetcher.rb2
-rw-r--r--lib/gitlab/cycle_analytics/production_helper.rb2
-rw-r--r--lib/gitlab/cycle_analytics/production_stage.rb2
-rw-r--r--lib/gitlab/cycle_analytics/review_event_fetcher.rb2
-rw-r--r--lib/gitlab/cycle_analytics/review_stage.rb2
-rw-r--r--lib/gitlab/cycle_analytics/stage.rb2
-rw-r--r--lib/gitlab/cycle_analytics/stage_summary.rb2
-rw-r--r--lib/gitlab/cycle_analytics/staging_event_fetcher.rb2
-rw-r--r--lib/gitlab/cycle_analytics/staging_stage.rb2
-rw-r--r--lib/gitlab/cycle_analytics/summary/base.rb2
-rw-r--r--lib/gitlab/cycle_analytics/summary/commit.rb2
-rw-r--r--lib/gitlab/cycle_analytics/summary/deploy.rb2
-rw-r--r--lib/gitlab/cycle_analytics/summary/issue.rb2
-rw-r--r--lib/gitlab/cycle_analytics/test_event_fetcher.rb2
-rw-r--r--lib/gitlab/cycle_analytics/test_stage.rb2
-rw-r--r--lib/gitlab/cycle_analytics/updater.rb2
-rw-r--r--lib/gitlab/cycle_analytics/usage_data.rb2
-rw-r--r--lib/gitlab/data_builder/build.rb2
-rw-r--r--lib/gitlab/data_builder/note.rb2
-rw-r--r--lib/gitlab/data_builder/pipeline.rb2
-rw-r--r--lib/gitlab/data_builder/push.rb2
-rw-r--r--lib/gitlab/data_builder/repository.rb2
-rw-r--r--lib/gitlab/data_builder/wiki_page.rb2
-rw-r--r--lib/gitlab/database/arel_methods.rb2
-rw-r--r--lib/gitlab/database/count.rb2
-rw-r--r--lib/gitlab/database/date_time.rb2
-rw-r--r--lib/gitlab/database/grant.rb2
-rw-r--r--lib/gitlab/database/median.rb2
-rw-r--r--lib/gitlab/database/migration_helpers.rb4
-rw-r--r--lib/gitlab/database/multi_threaded_migration.rb2
-rw-r--r--lib/gitlab/database/read_only_relation.rb2
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1.rb2
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb2
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb2
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb2
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb2
-rw-r--r--lib/gitlab/database/sha_attribute.rb2
-rw-r--r--lib/gitlab/dependency_linker/base_linker.rb2
-rw-r--r--lib/gitlab/dependency_linker/cartfile_linker.rb2
-rw-r--r--lib/gitlab/dependency_linker/cocoapods.rb2
-rw-r--r--lib/gitlab/dependency_linker/composer_json_linker.rb2
-rw-r--r--lib/gitlab/dependency_linker/gemfile_linker.rb2
-rw-r--r--lib/gitlab/dependency_linker/gemspec_linker.rb2
-rw-r--r--lib/gitlab/dependency_linker/godeps_json_linker.rb2
-rw-r--r--lib/gitlab/dependency_linker/json_linker.rb2
-rw-r--r--lib/gitlab/dependency_linker/method_linker.rb2
-rw-r--r--lib/gitlab/dependency_linker/package_json_linker.rb2
-rw-r--r--lib/gitlab/dependency_linker/podfile_linker.rb2
-rw-r--r--lib/gitlab/dependency_linker/podspec_json_linker.rb2
-rw-r--r--lib/gitlab/dependency_linker/podspec_linker.rb2
-rw-r--r--lib/gitlab/dependency_linker/requirements_txt_linker.rb2
-rw-r--r--lib/gitlab/diff/diff_refs.rb2
-rw-r--r--lib/gitlab/diff/file.rb21
-rw-r--r--lib/gitlab/diff/file_collection/base.rb9
-rw-r--r--lib/gitlab/diff/file_collection/commit.rb2
-rw-r--r--lib/gitlab/diff/file_collection/compare.rb2
-rw-r--r--lib/gitlab/diff/file_collection/merge_request_diff.rb2
-rw-r--r--lib/gitlab/diff/formatters/base_formatter.rb2
-rw-r--r--lib/gitlab/diff/formatters/image_formatter.rb2
-rw-r--r--lib/gitlab/diff/formatters/text_formatter.rb2
-rw-r--r--lib/gitlab/diff/highlight.rb4
-rw-r--r--lib/gitlab/diff/image_point.rb2
-rw-r--r--lib/gitlab/diff/inline_diff.rb4
-rw-r--r--lib/gitlab/diff/inline_diff_markdown_marker.rb2
-rw-r--r--lib/gitlab/diff/inline_diff_marker.rb2
-rw-r--r--lib/gitlab/diff/line.rb15
-rw-r--r--lib/gitlab/diff/line_mapper.rb2
-rw-r--r--lib/gitlab/diff/lines_unfolder.rb235
-rw-r--r--lib/gitlab/diff/parallel_diff.rb2
-rw-r--r--lib/gitlab/diff/parser.rb2
-rw-r--r--lib/gitlab/diff/position.rb14
-rw-r--r--lib/gitlab/diff/position_tracer.rb4
-rw-r--r--lib/gitlab/downtime_check/message.rb8
-rw-r--r--lib/gitlab/email/attachment_uploader.rb2
-rw-r--r--lib/gitlab/email/handler/create_merge_request_handler.rb50
-rw-r--r--lib/gitlab/email/hook/additional_headers_interceptor.rb2
-rw-r--r--lib/gitlab/email/hook/delivery_metrics_observer.rb2
-rw-r--r--lib/gitlab/email/hook/disable_email_interceptor.rb2
-rw-r--r--lib/gitlab/email/hook/email_template_interceptor.rb2
-rw-r--r--lib/gitlab/email/html_parser.rb2
-rw-r--r--lib/gitlab/email/message/repository_push.rb6
-rw-r--r--lib/gitlab/email/receiver.rb3
-rw-r--r--lib/gitlab/email/reply_parser.rb2
-rw-r--r--lib/gitlab/etag_caching/middleware.rb2
-rw-r--r--lib/gitlab/etag_caching/router.rb2
-rw-r--r--lib/gitlab/etag_caching/store.rb2
-rw-r--r--lib/gitlab/file_detector.rb2
-rw-r--r--lib/gitlab/gfm/reference_rewriter.rb22
-rw-r--r--lib/gitlab/gfm/uploads_rewriter.rb5
-rw-r--r--lib/gitlab/git/blob.rb3
-rw-r--r--lib/gitlab/git/commit.rb9
-rw-r--r--lib/gitlab/git/commit_stats.rb4
-rw-r--r--lib/gitlab/git/conflict/resolver.rb6
-rw-r--r--lib/gitlab/git/patches/collection.rb33
-rw-r--r--lib/gitlab/git/patches/commit_patches.rb31
-rw-r--r--lib/gitlab/git/patches/patch.rb19
-rw-r--r--lib/gitlab/git/remote_mirror.rb4
-rw-r--r--lib/gitlab/git/repository.rb65
-rw-r--r--lib/gitlab/git/tree.rb3
-rw-r--r--lib/gitlab/git/wiki.rb18
-rw-r--r--lib/gitlab/git/wraps_gitaly_errors.rb15
-rw-r--r--lib/gitlab/gitaly_client/operation_service.rb49
-rw-r--r--lib/gitlab/gitaly_client/repository_service.rb2
-rw-r--r--lib/gitlab/gon_helper.rb5
-rw-r--r--lib/gitlab/highlight.rb17
-rw-r--r--lib/gitlab/import/merge_request_helpers.rb2
-rw-r--r--lib/gitlab/import_export/import_export.yml1
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb2
-rw-r--r--lib/gitlab/kubernetes/helm.rb1
-rw-r--r--lib/gitlab/kubernetes/helm/api.rb30
-rw-r--r--lib/gitlab/kubernetes/helm/base_command.rb8
-rw-r--r--lib/gitlab/kubernetes/helm/init_command.rb2
-rw-r--r--lib/gitlab/kubernetes/helm/install_command.rb29
-rw-r--r--lib/gitlab/kubernetes/helm/pod.rb2
-rw-r--r--lib/gitlab/kubernetes/helm/upgrade_command.rb4
-rw-r--r--lib/gitlab/kubernetes/kube_client.rb3
-rw-r--r--lib/gitlab/kubernetes/namespace.rb4
-rw-r--r--lib/gitlab/kubernetes/role_binding.rb11
-rw-r--r--lib/gitlab/manifest_import/manifest.rb2
-rw-r--r--lib/gitlab/markup_helper.rb13
-rw-r--r--lib/gitlab/middleware/go.rb2
-rw-r--r--lib/gitlab/path_regex.rb2
-rw-r--r--lib/gitlab/private_commit_email.rb28
-rw-r--r--lib/gitlab/project_search_results.rb2
-rw-r--r--lib/gitlab/proxy_http_connection_adapter.rb2
-rw-r--r--lib/gitlab/quick_actions/command_definition.rb16
-rw-r--r--lib/gitlab/quick_actions/dsl.rb6
-rw-r--r--lib/gitlab/quick_actions/extractor.rb14
-rw-r--r--lib/gitlab/search_results.rb17
-rw-r--r--lib/gitlab/sentry.rb14
-rw-r--r--lib/gitlab/shell.rb17
-rw-r--r--lib/gitlab/slash_commands/issue_new.rb2
-rw-r--r--lib/gitlab/usage_data.rb19
-rw-r--r--lib/gitlab/user_extractor.rb4
-rw-r--r--lib/gitlab/utils.rb5
-rw-r--r--lib/google_api/auth.rb2
-rw-r--r--lib/json_web_token/hmac_token.rb28
-rw-r--r--lib/json_web_token/token.rb9
-rw-r--r--lib/tasks/gitlab/check.rake174
-rw-r--r--locale/ar_SA/gitlab.po2
-rw-r--r--locale/bg/gitlab.po2
-rw-r--r--locale/ca_ES/gitlab.po2
-rw-r--r--locale/cs_CZ/gitlab.po2
-rw-r--r--locale/da_DK/gitlab.po2
-rw-r--r--locale/de/gitlab.po2
-rw-r--r--locale/eo/gitlab.po2
-rw-r--r--locale/es/gitlab.po2
-rw-r--r--locale/et_EE/gitlab.po2
-rw-r--r--locale/fil_PH/gitlab.po2
-rw-r--r--locale/fr/gitlab.po2
-rw-r--r--locale/gitlab.pot173
-rw-r--r--locale/gl_ES/gitlab.po2
-rw-r--r--locale/he_IL/gitlab.po2
-rw-r--r--locale/id_ID/gitlab.po2
-rw-r--r--locale/it/gitlab.po2
-rw-r--r--locale/ja/gitlab.po2
-rw-r--r--locale/ko/gitlab.po2
-rw-r--r--locale/mn_MN/gitlab.po2
-rw-r--r--locale/nb_NO/gitlab.po2
-rw-r--r--locale/nl_NL/gitlab.po2
-rw-r--r--locale/pl_PL/gitlab.po2
-rw-r--r--locale/pt_BR/gitlab.po2
-rw-r--r--locale/ro_RO/gitlab.po2
-rw-r--r--locale/ru/gitlab.po2
-rw-r--r--locale/sq_AL/gitlab.po2
-rw-r--r--locale/tr_TR/gitlab.po2
-rw-r--r--locale/uk/gitlab.po2
-rw-r--r--locale/zh_CN/gitlab.po2
-rw-r--r--locale/zh_HK/gitlab.po2
-rw-r--r--locale/zh_TW/gitlab.po2
-rw-r--r--package.json4
-rw-r--r--qa/qa.rb61
-rw-r--r--qa/qa/factory/README.md445
-rw-r--r--qa/qa/factory/product.rb35
-rw-r--r--qa/qa/factory/resource/branch.rb77
-rw-r--r--qa/qa/factory/resource/ci_variable.rb30
-rw-r--r--qa/qa/factory/resource/deploy_key.rb43
-rw-r--r--qa/qa/factory/resource/deploy_token.rb50
-rw-r--r--qa/qa/factory/resource/file.rb38
-rw-r--r--qa/qa/factory/resource/fork.rb71
-rw-r--r--qa/qa/factory/resource/group.rb68
-rw-r--r--qa/qa/factory/resource/issue.rb30
-rw-r--r--qa/qa/factory/resource/kubernetes_cluster.rb57
-rw-r--r--qa/qa/factory/resource/label.rb39
-rw-r--r--qa/qa/factory/resource/merge_request.rb73
-rw-r--r--qa/qa/factory/resource/merge_request_from_fork.rb29
-rw-r--r--qa/qa/factory/resource/personal_access_token.rb27
-rw-r--r--qa/qa/factory/resource/project.rb78
-rw-r--r--qa/qa/factory/resource/project_imported_from_github.rb37
-rw-r--r--qa/qa/factory/resource/project_milestone.rb37
-rw-r--r--qa/qa/factory/resource/runner.rb49
-rw-r--r--qa/qa/factory/resource/sandbox.rb61
-rw-r--r--qa/qa/factory/resource/ssh_key.rb30
-rw-r--r--qa/qa/factory/resource/user.rb60
-rw-r--r--qa/qa/factory/resource/wiki.rb30
-rw-r--r--qa/qa/git/repository.rb26
-rw-r--r--qa/qa/page/main/login.rb2
-rw-r--r--qa/qa/page/merge_request/show.rb65
-rw-r--r--qa/qa/page/project/issue/show.rb5
-rw-r--r--qa/qa/page/project/operations/kubernetes/add.rb2
-rw-r--r--qa/qa/page/project/operations/kubernetes/add_existing.rb2
-rw-r--r--qa/qa/page/project/operations/kubernetes/index.rb2
-rw-r--r--qa/qa/resource/README.md392
-rw-r--r--qa/qa/resource/api_fabricator.rb (renamed from qa/qa/factory/api_fabricator.rb)16
-rw-r--r--qa/qa/resource/base.rb (renamed from qa/qa/factory/base.rb)38
-rw-r--r--qa/qa/resource/branch.rb77
-rw-r--r--qa/qa/resource/ci_variable.rb30
-rw-r--r--qa/qa/resource/deploy_key.rb43
-rw-r--r--qa/qa/resource/deploy_token.rb50
-rw-r--r--qa/qa/resource/file.rb36
-rw-r--r--qa/qa/resource/fork.rb43
-rw-r--r--qa/qa/resource/group.rb68
-rw-r--r--qa/qa/resource/issue.rb30
-rw-r--r--qa/qa/resource/kubernetes_cluster.rb57
-rw-r--r--qa/qa/resource/label.rb39
-rw-r--r--qa/qa/resource/merge_request.rb75
-rw-r--r--qa/qa/resource/merge_request_from_fork.rb31
-rw-r--r--qa/qa/resource/personal_access_token.rb27
-rw-r--r--qa/qa/resource/project.rb80
-rw-r--r--qa/qa/resource/project_imported_from_github.rb36
-rw-r--r--qa/qa/resource/project_milestone.rb36
-rw-r--r--qa/qa/resource/repository/project_push.rb (renamed from qa/qa/factory/repository/project_push.rb)10
-rw-r--r--qa/qa/resource/repository/push.rb (renamed from qa/qa/factory/repository/push.rb)6
-rw-r--r--qa/qa/resource/repository/wiki_push.rb (renamed from qa/qa/factory/repository/wiki_push.rb)8
-rw-r--r--qa/qa/resource/runner.rb49
-rw-r--r--qa/qa/resource/sandbox.rb60
-rw-r--r--qa/qa/resource/settings/hashed_storage.rb (renamed from qa/qa/factory/settings/hashed_storage.rb)6
-rw-r--r--qa/qa/resource/ssh_key.rb26
-rw-r--r--qa/qa/resource/user.rb92
-rw-r--r--qa/qa/resource/wiki.rb30
-rw-r--r--qa/qa/runtime/api/client.rb2
-rw-r--r--qa/qa/runtime/env.rb19
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb10
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb7
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb12
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb49
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb84
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb10
-rw-r--r--qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb8
-rw-r--r--qa/qa/specs/runner.rb4
-rw-r--r--qa/spec/factory/base_spec.rb248
-rw-r--r--qa/spec/factory/product_spec.rb29
-rw-r--r--qa/spec/factory/resource/user_spec.rb36
-rw-r--r--qa/spec/git/repository_spec.rb30
-rw-r--r--qa/spec/resource/api_fabricator_spec.rb (renamed from qa/spec/factory/api_fabricator_spec.rb)46
-rw-r--r--qa/spec/resource/base_spec.rb246
-rw-r--r--qa/spec/resource/repository/push_spec.rb (renamed from qa/spec/factory/repository/push_spec.rb)2
-rw-r--r--qa/spec/runtime/env_spec.rb47
-rw-r--r--qa/spec/specs/runner_spec.rb14
-rw-r--r--rubocop/cop/code_reuse/active_record.rb1
-rwxr-xr-xscripts/build_assets_image21
-rwxr-xr-xscripts/rails4-gemfile-lock-check19
-rwxr-xr-xscripts/rails5-gemfile-lock-check19
-rwxr-xr-xscripts/review_apps/review-apps.sh30
-rwxr-xr-xscripts/static-analysis1
-rwxr-xr-xscripts/trigger-build15
-rw-r--r--spec/controllers/concerns/send_file_upload_spec.rb31
-rw-r--r--spec/controllers/groups/boards_controller_spec.rb7
-rw-r--r--spec/controllers/groups/clusters/applications_controller_spec.rb87
-rw-r--r--spec/controllers/groups/clusters_controller_spec.rb574
-rw-r--r--spec/controllers/groups/milestones_controller_spec.rb4
-rw-r--r--spec/controllers/groups_controller_spec.rb2
-rw-r--r--spec/controllers/import/bitbucket_server_controller_spec.rb11
-rw-r--r--spec/controllers/profiles/keys_controller_spec.rb2
-rw-r--r--spec/controllers/projects/artifacts_controller_spec.rb33
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb6
-rw-r--r--spec/controllers/projects/boards_controller_spec.rb5
-rw-r--r--spec/controllers/projects/clusters/applications_controller_spec.rb2
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb196
-rw-r--r--spec/controllers/projects/commit_controller_spec.rb1
-rw-r--r--spec/controllers/projects/deployments_controller_spec.rb12
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb2
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb36
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb7
-rw-r--r--spec/controllers/projects/milestones_controller_spec.rb2
-rw-r--r--spec/factories/ci/builds.rb30
-rw-r--r--spec/factories/clusters/applications/helm.rb5
-rw-r--r--spec/factories/clusters/kubernetes_namespaces.rb16
-rw-r--r--spec/factories/clusters/platforms/kubernetes.rb2
-rw-r--r--spec/factories/deployments.rb26
-rw-r--r--spec/factories/environments.rb1
-rw-r--r--spec/factories/merge_request_diff_files.rb47
-rw-r--r--spec/factories/merge_request_diffs.rb13
-rw-r--r--spec/factories/services.rb11
-rw-r--r--spec/features/admin/admin_groups_spec.rb28
-rw-r--r--spec/features/boards/add_issues_modal_spec.rb2
-rw-r--r--spec/features/boards/issue_ordering_spec.rb4
-rw-r--r--spec/features/calendar_spec.rb2
-rw-r--r--spec/features/dashboard/archived_projects_spec.rb2
-rw-r--r--spec/features/dashboard/group_spec.rb8
-rw-r--r--spec/features/dashboard/project_member_activity_index_spec.rb16
-rw-r--r--spec/features/dashboard/projects_spec.rb8
-rw-r--r--spec/features/explore/new_menu_spec.rb4
-rw-r--r--spec/features/groups/board_sidebar_spec.rb45
-rw-r--r--spec/features/groups_spec.rb36
-rw-r--r--spec/features/import/manifest_import_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/dropdown_emoji_spec.rb20
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb2
-rw-r--r--spec/features/issues/user_views_issue_spec.rb6
-rw-r--r--spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb2
-rw-r--r--spec/features/merge_request/user_creates_image_diff_notes_spec.rb9
-rw-r--r--spec/features/merge_request/user_posts_diff_notes_spec.rb13
-rw-r--r--spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb15
-rw-r--r--spec/features/merge_request/user_sees_deployment_widget_spec.rb66
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb7
-rw-r--r--spec/features/merge_request/user_sees_pipelines_spec.rb3
-rw-r--r--spec/features/merge_request/user_sees_wip_help_message_spec.rb4
-rw-r--r--spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb11
-rw-r--r--spec/features/merge_request/user_uses_quick_actions_spec.rb2
-rw-r--r--spec/features/merge_requests/user_squashes_merge_request_spec.rb4
-rw-r--r--spec/features/milestones/user_creates_milestone_spec.rb2
-rw-r--r--spec/features/milestones/user_deletes_milestone_spec.rb2
-rw-r--r--spec/features/projects/activity/user_sees_activity_spec.rb6
-rw-r--r--spec/features/projects/activity/user_sees_private_activity_spec.rb2
-rw-r--r--spec/features/projects/badges/pipeline_badge_spec.rb2
-rw-r--r--spec/features/projects/clusters/gcp_spec.rb1
-rw-r--r--spec/features/projects/clusters/user_spec.rb2
-rw-r--r--spec/features/projects/clusters_spec.rb31
-rw-r--r--spec/features/projects/environments/environment_spec.rb63
-rw-r--r--spec/features/projects/environments/environments_spec.rb97
-rw-r--r--spec/features/projects/files/user_creates_directory_spec.rb5
-rw-r--r--spec/features/projects/files/user_creates_files_spec.rb8
-rw-r--r--spec/features/projects/files/user_deletes_files_spec.rb2
-rw-r--r--spec/features/projects/files/user_edits_files_spec.rb6
-rw-r--r--spec/features/projects/files/user_replaces_files_spec.rb2
-rw-r--r--spec/features/projects/files/user_uploads_files_spec.rb4
-rw-r--r--spec/features/projects/jobs_spec.rb34
-rw-r--r--spec/features/projects/merge_request_button_spec.rb8
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb57
-rw-r--r--spec/features/projects/view_on_env_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_wiki_pages_spec.rb2
-rw-r--r--spec/features/user_sorts_things_spec.rb2
-rw-r--r--spec/finders/environments_finder_spec.rb16
-rw-r--r--spec/finders/issues_finder_spec.rb32
-rw-r--r--spec/finders/notes_finder_spec.rb10
-rw-r--r--spec/finders/personal_access_tokens_finder_spec.rb8
-rw-r--r--spec/finders/snippets_finder_spec.rb45
-rw-r--r--spec/fixtures/api/schemas/deployment.json4
-rw-r--r--spec/fixtures/api/schemas/entities/issue_board.json39
-rw-r--r--spec/fixtures/api/schemas/issue.json2
-rw-r--r--spec/fixtures/api/schemas/job/job.json6
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/merge_requests.json26
-rw-r--r--spec/fixtures/emails/merge_request_multiple_patches.eml181
-rw-r--r--spec/fixtures/emails/merge_request_with_conflicting_patch.eml45
-rw-r--r--spec/fixtures/emails/merge_request_with_patch_and_target_branch.eml44
-rw-r--r--spec/fixtures/emails/valid_merge_request_with_patch.eml151
-rw-r--r--spec/fixtures/patchfiles/0001-A-commit-from-a-patch.patch19
-rw-r--r--spec/fixtures/patchfiles/0001-This-does-not-apply-to-the-feature-branch.patch23
-rw-r--r--spec/helpers/blob_helper_spec.rb58
-rw-r--r--spec/helpers/events_helper_spec.rb46
-rw-r--r--spec/helpers/labels_helper_spec.rb25
-rw-r--r--spec/helpers/profiles_helper_spec.rb34
-rw-r--r--spec/helpers/tree_helper_spec.rb45
-rw-r--r--spec/javascripts/awards_handler_spec.js4
-rw-r--r--spec/javascripts/boards/board_list_spec.js86
-rw-r--r--spec/javascripts/boards/components/board_spec.js37
-rw-r--r--spec/javascripts/boards/components/issue_due_date_spec.js64
-rw-r--r--spec/javascripts/boards/components/issue_time_estimate_spec.js40
-rw-r--r--spec/javascripts/boards/issue_card_spec.js25
-rw-r--r--spec/javascripts/clusters/components/applications_spec.js12
-rw-r--r--spec/javascripts/clusters/services/mock_data.js11
-rw-r--r--spec/javascripts/clusters/stores/clusters_store_spec.js8
-rw-r--r--spec/javascripts/commit/commit_pipeline_status_component_spec.js8
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js23
-rw-r--r--spec/javascripts/diffs/components/commit_item_spec.js30
-rw-r--r--spec/javascripts/diffs/components/commit_widget_spec.js2
-rw-r--r--spec/javascripts/diffs/components/diff_content_spec.js77
-rw-r--r--spec/javascripts/diffs/components/diff_discussions_spec.js78
-rw-r--r--spec/javascripts/diffs/components/diff_file_header_spec.js91
-rw-r--r--spec/javascripts/diffs/components/diff_file_spec.js34
-rw-r--r--spec/javascripts/diffs/components/diff_line_gutter_content_spec.js25
-rw-r--r--spec/javascripts/diffs/components/diff_line_note_form_spec.js6
-rw-r--r--spec/javascripts/diffs/components/image_diff_overlay_spec.js146
-rw-r--r--spec/javascripts/diffs/components/inline_diff_view_spec.js9
-rw-r--r--spec/javascripts/diffs/components/parallel_diff_view_spec.js2
-rw-r--r--spec/javascripts/diffs/components/tree_list_spec.js2
-rw-r--r--spec/javascripts/diffs/mock_data/diff_discussions.js21
-rw-r--r--spec/javascripts/diffs/mock_data/diff_file.js239
-rw-r--r--spec/javascripts/diffs/mock_data/diff_with_commit.js4
-rw-r--r--spec/javascripts/diffs/store/actions_spec.js86
-rw-r--r--spec/javascripts/diffs/store/getters_spec.js18
-rw-r--r--spec/javascripts/diffs/store/mutations_spec.js122
-rw-r--r--spec/javascripts/diffs/store/utils_spec.js186
-rw-r--r--spec/javascripts/dirty_submit/dirty_submit_form_spec.js32
-rw-r--r--spec/javascripts/environments/emtpy_state_spec.js23
-rw-r--r--spec/javascripts/environments/environment_actions_spec.js126
-rw-r--r--spec/javascripts/environments/environment_item_spec.js56
-rw-r--r--spec/javascripts/environments/environments_app_spec.js89
-rw-r--r--spec/javascripts/environments/folder/environments_folder_view_spec.js104
-rw-r--r--spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js57
-rw-r--r--spec/javascripts/fixtures/jobs.rb23
-rw-r--r--spec/javascripts/issue_show/components/title_spec.js18
-rw-r--r--spec/javascripts/jobs/components/job_app_spec.js73
-rw-r--r--spec/javascripts/jobs/components/job_container_item_spec.js26
-rw-r--r--spec/javascripts/jobs/mixins/delayed_job_mixin_spec.js93
-rw-r--r--spec/javascripts/jobs/store/actions_spec.js36
-rw-r--r--spec/javascripts/jobs/store/getters_spec.js6
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js221
-rw-r--r--spec/javascripts/lib/utils/datetime_utility_spec.js10
-rw-r--r--spec/javascripts/lib/utils/number_utility_spec.js8
-rw-r--r--spec/javascripts/lib/utils/text_utility_spec.js2
-rw-r--r--spec/javascripts/monitoring/graph/flag_spec.js33
-rw-r--r--spec/javascripts/notes/components/diff_with_note_spec.js7
-rw-r--r--spec/javascripts/notes/components/discussion_filter_spec.js44
-rw-r--r--spec/javascripts/notes/components/note_actions_spec.js2
-rw-r--r--spec/javascripts/notes/components/note_app_spec.js10
-rw-r--r--spec/javascripts/notes/components/noteable_discussion_spec.js16
-rw-r--r--spec/javascripts/notes/components/toggle_replies_widget_spec.js78
-rw-r--r--spec/javascripts/notes/stores/actions_spec.js16
-rw-r--r--spec/javascripts/notes/stores/mutation_spec.js12
-rw-r--r--spec/javascripts/pipelines/empty_state_spec.js2
-rw-r--r--spec/javascripts/pipelines/graph/action_component_spec.js2
-rw-r--r--spec/javascripts/pipelines/graph/graph_component_spec.js8
-rw-r--r--spec/javascripts/pipelines/graph/job_item_spec.js35
-rw-r--r--spec/javascripts/pipelines/header_component_spec.js2
-rw-r--r--spec/javascripts/pipelines/pipeline_url_spec.js5
-rw-r--r--spec/javascripts/pipelines/pipelines_spec.js4
-rw-r--r--spec/javascripts/pipelines/pipelines_table_row_spec.js8
-rw-r--r--spec/javascripts/pipelines/stage_spec.js2
-rw-r--r--spec/javascripts/reports/components/grouped_test_reports_app_spec.js4
-rw-r--r--spec/javascripts/reports/components/report_section_spec.js2
-rw-r--r--spec/javascripts/sidebar/assignees_spec.js84
-rw-r--r--spec/javascripts/vue_mr_widget/components/deployment_spec.js34
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js79
-rw-r--r--spec/javascripts/vue_mr_widget/components/review_app_link_spec.js38
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/mock_data.js4
-rw-r--r--spec/javascripts/vue_mr_widget/mr_widget_options_spec.js63
-rw-r--r--spec/javascripts/vue_shared/components/clipboard_button_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/commit_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js36
-rw-r--r--spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/header_ci_component_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js8
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js12
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/smart_virtual_list_spec.js83
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js57
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js30
-rw-r--r--spec/lib/banzai/filter/autolink_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/external_issue_reference_filter_spec.rb28
-rw-r--r--spec/lib/banzai/filter/relative_link_filter_spec.rb2
-rw-r--r--spec/lib/bitbucket_server/client_spec.rb12
-rw-r--r--spec/lib/bitbucket_server/collection_spec.rb29
-rw-r--r--spec/lib/bitbucket_server/paginator_spec.rb10
-rw-r--r--spec/lib/container_registry/blob_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb97
-rw-r--r--spec/lib/gitlab/checks/lfs_integrity_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/ansi2html_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/build/policy/variables_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/global_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb34
-rw-r--r--spec/lib/gitlab/ci/config/entry/reports_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/retry_spec.rb236
-rw-r--r--spec/lib/gitlab/ci/config/normalizer_spec.rb66
-rw-r--r--spec/lib/gitlab/ci/status/build/scheduled_spec.rb20
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb31
-rw-r--r--spec/lib/gitlab/cross_project_access/check_info_spec.rb4
-rw-r--r--spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb6
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb7
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb46
-rw-r--r--spec/lib/gitlab/diff/lines_unfolder_spec.rb750
-rw-r--r--spec/lib/gitlab/diff/position_spec.rb8
-rw-r--r--spec/lib/gitlab/diff/position_tracer_spec.rb2
-rw-r--r--spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb69
-rw-r--r--spec/lib/gitlab/file_detector_spec.rb7
-rw-r--r--spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb2
-rw-r--r--spec/lib/gitlab/git/attributes_parser_spec.rb4
-rw-r--r--spec/lib/gitlab/git/patches/collection_spec.rb28
-rw-r--r--spec/lib/gitlab/git/patches/commit_patches_spec.rb49
-rw-r--r--spec/lib/gitlab/git/patches/patch_spec.rb16
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb63
-rw-r--r--spec/lib/gitlab/git/wraps_gitaly_errors_spec.rb28
-rw-r--r--spec/lib/gitlab/gitaly_client/operation_service_spec.rb33
-rw-r--r--spec/lib/gitlab/gitaly_client/repository_service_spec.rb61
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb7
-rw-r--r--spec/lib/gitlab/gpg_spec.rb4
-rw-r--r--spec/lib/gitlab/highlight_spec.rb83
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml2
-rw-r--r--spec/lib/gitlab/import_export/attribute_configuration_spec.rb15
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/helm/api_spec.rb27
-rw-r--r--spec/lib/gitlab/kubernetes/helm/init_command_spec.rb4
-rw-r--r--spec/lib/gitlab/kubernetes/helm/install_command_spec.rb76
-rw-r--r--spec/lib/gitlab/kubernetes/helm/pod_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/helm/upgrade_command_spec.rb16
-rw-r--r--spec/lib/gitlab/kubernetes/kube_client_spec.rb14
-rw-r--r--spec/lib/gitlab/kubernetes/namespace_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/role_binding_spec.rb3
-rw-r--r--spec/lib/gitlab/private_commit_email_spec.rb41
-rw-r--r--spec/lib/gitlab/quick_actions/command_definition_spec.rb13
-rw-r--r--spec/lib/gitlab/quick_actions/dsl_spec.rb8
-rw-r--r--spec/lib/gitlab/quick_actions/extractor_spec.rb19
-rw-r--r--spec/lib/gitlab/sentry_spec.rb24
-rw-r--r--spec/lib/gitlab/shell_spec.rb120
-rw-r--r--spec/lib/gitlab/slash_commands/command_spec.rb2
-rw-r--r--spec/lib/gitlab/slash_commands/deploy_spec.rb2
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb10
-rw-r--r--spec/lib/gitlab/utils_spec.rb23
-rw-r--r--spec/lib/gitlab/view/presenter/base_spec.rb2
-rw-r--r--spec/lib/json_web_token/hmac_token_spec.rb133
-rw-r--r--spec/lib/microsoft_teams/notifier_spec.rb2
-rw-r--r--spec/migrations/delete_inconsistent_internal_id_records_spec.rb15
-rw-r--r--spec/migrations/fill_empty_finished_at_in_deployments_spec.rb70
-rw-r--r--spec/migrations/migrate_old_artifacts_spec.rb2
-rw-r--r--spec/migrations/steal_fill_store_upload_spec.rb40
-rw-r--r--spec/models/application_setting_spec.rb31
-rw-r--r--spec/models/ci/build_spec.rb382
-rw-r--r--spec/models/ci/pipeline_spec.rb18
-rw-r--r--spec/models/clusters/applications/ingress_spec.rb24
-rw-r--r--spec/models/clusters/applications/jupyter_spec.rb24
-rw-r--r--spec/models/clusters/applications/knative_spec.rb77
-rw-r--r--spec/models/clusters/applications/prometheus_spec.rb34
-rw-r--r--spec/models/clusters/applications/runner_spec.rb24
-rw-r--r--spec/models/clusters/cluster_spec.rb26
-rw-r--r--spec/models/clusters/kubernetes_namespace_spec.rb101
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb103
-rw-r--r--spec/models/compare_spec.rb29
-rw-r--r--spec/models/concerns/awardable_spec.rb18
-rw-r--r--spec/models/concerns/blob_language_from_git_attributes_spec.rb25
-rw-r--r--spec/models/concerns/cacheable_attributes_spec.rb4
-rw-r--r--spec/models/concerns/deployable_spec.rb74
-rw-r--r--spec/models/concerns/each_batch_spec.rb51
-rw-r--r--spec/models/concerns/issuable_spec.rb4
-rw-r--r--spec/models/deployment_spec.rb192
-rw-r--r--spec/models/environment_spec.rb105
-rw-r--r--spec/models/environment_status_spec.rb92
-rw-r--r--spec/models/lfs_object_spec.rb2
-rw-r--r--spec/models/merge_request_diff_spec.rb46
-rw-r--r--spec/models/merge_request_spec.rb94
-rw-r--r--spec/models/namespace_spec.rb11
-rw-r--r--spec/models/postgresql/replication_slot_spec.rb20
-rw-r--r--spec/models/project_services/discord_service_spec.rb11
-rw-r--r--spec/models/project_services/hangouts_chat_service_spec.rb249
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb6
-rw-r--r--spec/models/project_spec.rb80
-rw-r--r--spec/models/project_wiki_spec.rb4
-rw-r--r--spec/models/shard_spec.rb50
-rw-r--r--spec/models/snippet_spec.rb211
-rw-r--r--spec/models/upload_spec.rb64
-rw-r--r--spec/models/user_preference_spec.rb37
-rw-r--r--spec/models/user_spec.rb79
-rw-r--r--spec/models/wiki_page_spec.rb23
-rw-r--r--spec/policies/clusters/cluster_policy_spec.rb42
-rw-r--r--spec/policies/group_policy_spec.rb6
-rw-r--r--spec/policies/project_policy_spec.rb4
-rw-r--r--spec/presenters/blob_presenter_spec.rb44
-rw-r--r--spec/presenters/ci/build_presenter_spec.rb2
-rw-r--r--spec/presenters/clusterable_presenter_spec.rb17
-rw-r--r--spec/presenters/clusters/cluster_presenter_spec.rb21
-rw-r--r--spec/presenters/group_clusterable_presenter_spec.rb77
-rw-r--r--spec/presenters/project_clusterable_presenter_spec.rb77
-rw-r--r--spec/presenters/project_presenter_spec.rb2
-rw-r--r--spec/requests/api/commits_spec.rb114
-rw-r--r--spec/requests/api/deployments_spec.rb12
-rw-r--r--spec/requests/api/internal_spec.rb6
-rw-r--r--spec/requests/api/issues_spec.rb92
-rw-r--r--spec/requests/api/submodules_spec.rb99
-rw-r--r--spec/serializers/build_action_entity_spec.rb4
-rw-r--r--spec/serializers/deployment_entity_spec.rb22
-rw-r--r--spec/serializers/environment_serializer_spec.rb3
-rw-r--r--spec/serializers/environment_status_entity_spec.rb43
-rw-r--r--spec/serializers/job_entity_spec.rb1
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb93
-rw-r--r--spec/services/ci/process_build_service_spec.rb44
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb4
-rw-r--r--spec/services/ci/register_job_service_spec.rb14
-rw-r--r--spec/services/ci/retry_build_service_spec.rb2
-rw-r--r--spec/services/ci/run_scheduled_build_service_spec.rb4
-rw-r--r--spec/services/clusters/applications/check_installation_progress_service_spec.rb31
-rw-r--r--spec/services/clusters/applications/create_service_spec.rb59
-rw-r--r--spec/services/clusters/applications/install_service_spec.rb2
-rw-r--r--spec/services/clusters/create_service_spec.rb31
-rw-r--r--spec/services/clusters/gcp/fetch_operation_service_spec.rb2
-rw-r--r--spec/services/clusters/gcp/finalize_creation_service_spec.rb278
-rw-r--r--spec/services/clusters/gcp/kubernetes/create_or_update_namespace_service_spec.rb115
-rw-r--r--spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb211
-rw-r--r--spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb57
-rw-r--r--spec/services/clusters/gcp/provision_service_spec.rb2
-rw-r--r--spec/services/clusters/update_service_spec.rb34
-rw-r--r--spec/services/commits/commit_patch_service_spec.rb92
-rw-r--r--spec/services/create_deployment_service_spec.rb335
-rw-r--r--spec/services/groups/transfer_service_spec.rb4
-rw-r--r--spec/services/issuable/bulk_update_service_spec.rb2
-rw-r--r--spec/services/issuable/clone/attributes_rewriter_spec.rb79
-rw-r--r--spec/services/issuable/clone/content_rewriter_spec.rb153
-rw-r--r--spec/services/issues/move_service_spec.rb268
-rw-r--r--spec/services/issues/update_service_spec.rb48
-rw-r--r--spec/services/merge_requests/build_service_spec.rb8
-rw-r--r--spec/services/merge_requests/get_urls_service_spec.rb4
-rw-r--r--spec/services/merge_requests/reload_diffs_service_spec.rb32
-rw-r--r--spec/services/merge_requests/update_service_spec.rb50
-rw-r--r--spec/services/milestones/destroy_service_spec.rb2
-rw-r--r--spec/services/notes/create_service_spec.rb51
-rw-r--r--spec/services/notes/destroy_service_spec.rb33
-rw-r--r--spec/services/notes/quick_actions_service_spec.rb2
-rw-r--r--spec/services/notification_service_spec.rb160
-rw-r--r--spec/services/projects/import_service_spec.rb2
-rw-r--r--spec/services/projects/transfer_service_spec.rb2
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb13
-rw-r--r--spec/services/submodules/update_service_spec.rb212
-rw-r--r--spec/services/todo_service_spec.rb2
-rw-r--r--spec/services/update_deployment_service_spec.rb217
-rw-r--r--spec/services/users/build_service_spec.rb6
-rw-r--r--spec/spec_helper.rb4
-rw-r--r--spec/support/capybara.rb2
-rw-r--r--spec/support/features/discussion_comments_shared_example.rb14
-rw-r--r--spec/support/features/reportable_note_shared_examples.rb4
-rw-r--r--spec/support/helpers/cycle_analytics_helpers.rb8
-rw-r--r--spec/support/helpers/filtered_search_helpers.rb8
-rw-r--r--spec/support/helpers/kubernetes_helpers.rb17
-rw-r--r--spec/support/helpers/migrations_helpers.rb22
-rw-r--r--spec/support/helpers/project_forks_helper.rb2
-rw-r--r--spec/support/helpers/seed_helper.rb20
-rw-r--r--spec/support/helpers/test_env.rb11
-rw-r--r--spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/helm_generated_script.rb10
-rw-r--r--spec/support/shared_examples/models/chat_service_spec.rb242
-rw-r--r--spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb25
-rw-r--r--spec/support/shared_examples/requests/api/merge_requests_list.rb7
-rw-r--r--spec/support/shared_examples/services/boards/issues_move_service.rb2
-rw-r--r--spec/support/shared_examples/services/boards/lists_move_service.rb12
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/uploads/migrate_rake_spec.rb8
-rw-r--r--spec/uploaders/file_uploader_spec.rb27
-rw-r--r--spec/uploaders/namespace_file_uploader_spec.rb58
-rw-r--r--spec/views/projects/blob/_viewer.html.haml_spec.rb2
-rw-r--r--spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb8
-rw-r--r--spec/views/projects/tree/_tree_row.html.haml_spec.rb (renamed from spec/views/projects/tree/_blob_item.html.haml_spec.rb)9
-rw-r--r--spec/workers/build_success_worker_spec.rb54
-rw-r--r--spec/workers/cluster_platform_configure_worker_spec.rb47
-rw-r--r--spec/workers/cluster_provision_worker_spec.rb9
-rw-r--r--spec/workers/deployments/success_worker_spec.rb36
-rw-r--r--spec/workers/email_receiver_worker_spec.rb15
-rw-r--r--spec/workers/stuck_import_jobs_worker_spec.rb22
-rw-r--r--vendor/gitignore/Android.gitignore4
-rw-r--r--vendor/gitignore/Delphi.gitignore3
-rw-r--r--vendor/gitignore/Elixir.gitignore1
-rw-r--r--vendor/gitignore/Global/Images.gitignore63
-rw-r--r--vendor/gitignore/Global/NetBeans.gitignore2
-rw-r--r--vendor/gitignore/Global/PSoCCreator.gitignore18
-rw-r--r--vendor/gitignore/Global/Xcode.gitignore74
-rw-r--r--vendor/gitignore/Laravel.gitignore2
-rw-r--r--vendor/gitignore/Magento.gitignore2
-rw-r--r--vendor/gitignore/Node.gitignore3
-rw-r--r--vendor/gitignore/Python.gitignore3
-rw-r--r--vendor/gitignore/Rails.gitignore1
-rw-r--r--vendor/gitignore/Unity.gitignore1
-rw-r--r--vendor/licenses.csv54
-rw-r--r--yarn.lock18
1766 files changed, 30687 insertions, 13097 deletions
diff --git a/.gitattributes b/.gitattributes
index f1c41c9bb76..7282c9e61b1 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1 +1,2 @@
Dangerfile gitlab-language=ruby
+db/schema.rb merge=merge_db_schema
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 0d70eae0d1e..2595d0bbe1b 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,4 @@
-image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.5-golang-1.9-git-2.18-chrome-69.0-node-8.x-yarn-1.2-postgresql-9.6-graphicsmagick-1.3.29"
+image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-golang-1.9-git-2.18-chrome-69.0-node-10.x-yarn-1.12-postgresql-9.6-graphicsmagick-1.3.29"
.dedicated-runner: &dedicated-runner
retry: 1
@@ -6,7 +6,7 @@ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.5-golang-1.9-git
- gitlab-org
.default-cache: &default-cache
- key: "ruby-2.4.5-debian-stretch-with-yarn"
+ key: "debian-stretch-ruby-2.5.3-node-10.x"
paths:
- vendor/ruby
- .yarn-cache/
@@ -75,15 +75,17 @@ stages:
- mysql:5.7
- redis:alpine
-.rails5: &rails5
- allow_failure: true
- only:
+.rails4: &rails4
+ allow_failure: false
+ except:
variables:
- - $CI_COMMIT_REF_NAME =~ /rails5/
- - $RAILS5_ENABLED
+ - $CI_COMMIT_REF_NAME =~ /(^docs[\/-].*|.*-docs$)/
+ - $CI_COMMIT_REF_NAME =~ /(^qa[\/-].*|.*-qa$)/
+ - $CI_COMMIT_REF_NAME =~ /norails4/
+ - $RAILS5_DISABLED
variables:
- BUNDLE_GEMFILE: "Gemfile.rails5"
- RAILS5: "true"
+ BUNDLE_GEMFILE: "Gemfile.rails4"
+ RAILS5: "false"
# Skip all jobs except the ones that begin with 'docs/'.
# Used for commits including ONLY documentation changes.
@@ -121,7 +123,7 @@ stages:
<<: *except-docs-and-qa
.single-script-job: &single-script-job
- image: ruby:2.4-alpine
+ image: ruby:2.5-alpine
stage: test
cache: {}
dependencies: []
@@ -177,17 +179,17 @@ stages:
<<: *rspec-metadata
<<: *use-pg
-.rspec-metadata-pg-rails5: &rspec-metadata-pg-rails5
+.rspec-metadata-pg-rails4: &rspec-metadata-pg-rails4
<<: *rspec-metadata-pg
- <<: *rails5
+ <<: *rails4
.rspec-metadata-mysql: &rspec-metadata-mysql
<<: *rspec-metadata
<<: *use-mysql
-.rspec-metadata-mysql-rails5: &rspec-metadata-mysql-rails5
+.rspec-metadata-mysql-rails4: &rspec-metadata-mysql-rails4
<<: *rspec-metadata-mysql
- <<: *rails5
+ <<: *rails4
.only-canonical-masters: &only-canonical-masters
only:
@@ -227,6 +229,8 @@ stages:
script:
- git fetch https://gitlab.com/gitlab-org/gitlab-ce.git v9.3.0
- git checkout -f FETCH_HEAD
+ - sed -i "s/gem 'oj', '~> 2.17.4'//" Gemfile
+ - bundle update google-protobuf grpc
- bundle install $BUNDLE_INSTALL_FLAGS
- date
- cp config/gitlab.yml.example config/gitlab.yml
@@ -316,9 +320,10 @@ review-docs-cleanup:
# Trigger a docker image build in CNG (Cloud Native GitLab) repository
#
cloud-native-image:
- image: ruby:2.4-alpine
+ image: ruby:2.5-alpine
before_script: []
- stage: test
+ dependencies: []
+ stage: post-test
allow_failure: true
variables:
GIT_DEPTH: "1"
@@ -368,7 +373,7 @@ update-tests-metadata:
flaky-examples-check:
<<: *dedicated-runner
- image: ruby:2.4-alpine
+ image: ruby:2.5-alpine
services: []
before_script: []
variables:
@@ -426,7 +431,7 @@ setup-test-env:
script:
- bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
- scripts/gitaly-test-build # Do not use 'bundle exec' here
- - BUNDLE_GEMFILE=Gemfile.rails5 bundle install $BUNDLE_INSTALL_FLAGS
+ - BUNDLE_GEMFILE=Gemfile.rails4 bundle install $BUNDLE_INSTALL_FLAGS
artifacts:
expire_in: 7d
paths:
@@ -518,67 +523,67 @@ rspec-mysql 27 30: *rspec-metadata-mysql
rspec-mysql 28 30: *rspec-metadata-mysql
rspec-mysql 29 30: *rspec-metadata-mysql
-rspec-pg-rails5 0 30: *rspec-metadata-pg-rails5
-rspec-pg-rails5 1 30: *rspec-metadata-pg-rails5
-rspec-pg-rails5 2 30: *rspec-metadata-pg-rails5
-rspec-pg-rails5 3 30: *rspec-metadata-pg-rails5
-rspec-pg-rails5 4 30: *rspec-metadata-pg-rails5
-rspec-pg-rails5 5 30: *rspec-metadata-pg-rails5
-rspec-pg-rails5 6 30: *rspec-metadata-pg-rails5
-rspec-pg-rails5 7 30: *rspec-metadata-pg-rails5
-rspec-pg-rails5 8 30: *rspec-metadata-pg-rails5
-rspec-pg-rails5 9 30: *rspec-metadata-pg-rails5
-rspec-pg-rails5 10 30: *rspec-metadata-pg-rails5
-rspec-pg-rails5 11 30: *rspec-metadata-pg-rails5
-rspec-pg-rails5 12 30: *rspec-metadata-pg-rails5
-rspec-pg-rails5 13 30: *rspec-metadata-pg-rails5
-rspec-pg-rails5 14 30: *rspec-metadata-pg-rails5
-rspec-pg-rails5 15 30: *rspec-metadata-pg-rails5
-rspec-pg-rails5 16 30: *rspec-metadata-pg-rails5
-rspec-pg-rails5 17 30: *rspec-metadata-pg-rails5
-rspec-pg-rails5 18 30: *rspec-metadata-pg-rails5
-rspec-pg-rails5 19 30: *rspec-metadata-pg-rails5
-rspec-pg-rails5 20 30: *rspec-metadata-pg-rails5
-rspec-pg-rails5 21 30: *rspec-metadata-pg-rails5
-rspec-pg-rails5 22 30: *rspec-metadata-pg-rails5
-rspec-pg-rails5 23 30: *rspec-metadata-pg-rails5
-rspec-pg-rails5 24 30: *rspec-metadata-pg-rails5
-rspec-pg-rails5 25 30: *rspec-metadata-pg-rails5
-rspec-pg-rails5 26 30: *rspec-metadata-pg-rails5
-rspec-pg-rails5 27 30: *rspec-metadata-pg-rails5
-rspec-pg-rails5 28 30: *rspec-metadata-pg-rails5
-rspec-pg-rails5 29 30: *rspec-metadata-pg-rails5
-
-rspec-mysql-rails5 0 30: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 1 30: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 2 30: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 3 30: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 4 30: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 5 30: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 6 30: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 7 30: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 8 30: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 9 30: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 10 30: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 11 30: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 12 30: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 13 30: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 14 30: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 15 30: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 16 30: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 17 30: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 18 30: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 19 30: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 20 30: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 21 30: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 22 30: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 23 30: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 24 30: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 25 30: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 26 30: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 27 30: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 28 30: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 29 30: *rspec-metadata-mysql-rails5
+rspec-pg-rails4 0 30: *rspec-metadata-pg-rails4
+rspec-pg-rails4 1 30: *rspec-metadata-pg-rails4
+rspec-pg-rails4 2 30: *rspec-metadata-pg-rails4
+rspec-pg-rails4 3 30: *rspec-metadata-pg-rails4
+rspec-pg-rails4 4 30: *rspec-metadata-pg-rails4
+rspec-pg-rails4 5 30: *rspec-metadata-pg-rails4
+rspec-pg-rails4 6 30: *rspec-metadata-pg-rails4
+rspec-pg-rails4 7 30: *rspec-metadata-pg-rails4
+rspec-pg-rails4 8 30: *rspec-metadata-pg-rails4
+rspec-pg-rails4 9 30: *rspec-metadata-pg-rails4
+rspec-pg-rails4 10 30: *rspec-metadata-pg-rails4
+rspec-pg-rails4 11 30: *rspec-metadata-pg-rails4
+rspec-pg-rails4 12 30: *rspec-metadata-pg-rails4
+rspec-pg-rails4 13 30: *rspec-metadata-pg-rails4
+rspec-pg-rails4 14 30: *rspec-metadata-pg-rails4
+rspec-pg-rails4 15 30: *rspec-metadata-pg-rails4
+rspec-pg-rails4 16 30: *rspec-metadata-pg-rails4
+rspec-pg-rails4 17 30: *rspec-metadata-pg-rails4
+rspec-pg-rails4 18 30: *rspec-metadata-pg-rails4
+rspec-pg-rails4 19 30: *rspec-metadata-pg-rails4
+rspec-pg-rails4 20 30: *rspec-metadata-pg-rails4
+rspec-pg-rails4 21 30: *rspec-metadata-pg-rails4
+rspec-pg-rails4 22 30: *rspec-metadata-pg-rails4
+rspec-pg-rails4 23 30: *rspec-metadata-pg-rails4
+rspec-pg-rails4 24 30: *rspec-metadata-pg-rails4
+rspec-pg-rails4 25 30: *rspec-metadata-pg-rails4
+rspec-pg-rails4 26 30: *rspec-metadata-pg-rails4
+rspec-pg-rails4 27 30: *rspec-metadata-pg-rails4
+rspec-pg-rails4 28 30: *rspec-metadata-pg-rails4
+rspec-pg-rails4 29 30: *rspec-metadata-pg-rails4
+
+rspec-mysql-rails4 0 30: *rspec-metadata-mysql-rails4
+rspec-mysql-rails4 1 30: *rspec-metadata-mysql-rails4
+rspec-mysql-rails4 2 30: *rspec-metadata-mysql-rails4
+rspec-mysql-rails4 3 30: *rspec-metadata-mysql-rails4
+rspec-mysql-rails4 4 30: *rspec-metadata-mysql-rails4
+rspec-mysql-rails4 5 30: *rspec-metadata-mysql-rails4
+rspec-mysql-rails4 6 30: *rspec-metadata-mysql-rails4
+rspec-mysql-rails4 7 30: *rspec-metadata-mysql-rails4
+rspec-mysql-rails4 8 30: *rspec-metadata-mysql-rails4
+rspec-mysql-rails4 9 30: *rspec-metadata-mysql-rails4
+rspec-mysql-rails4 10 30: *rspec-metadata-mysql-rails4
+rspec-mysql-rails4 11 30: *rspec-metadata-mysql-rails4
+rspec-mysql-rails4 12 30: *rspec-metadata-mysql-rails4
+rspec-mysql-rails4 13 30: *rspec-metadata-mysql-rails4
+rspec-mysql-rails4 14 30: *rspec-metadata-mysql-rails4
+rspec-mysql-rails4 15 30: *rspec-metadata-mysql-rails4
+rspec-mysql-rails4 16 30: *rspec-metadata-mysql-rails4
+rspec-mysql-rails4 17 30: *rspec-metadata-mysql-rails4
+rspec-mysql-rails4 18 30: *rspec-metadata-mysql-rails4
+rspec-mysql-rails4 19 30: *rspec-metadata-mysql-rails4
+rspec-mysql-rails4 20 30: *rspec-metadata-mysql-rails4
+rspec-mysql-rails4 21 30: *rspec-metadata-mysql-rails4
+rspec-mysql-rails4 22 30: *rspec-metadata-mysql-rails4
+rspec-mysql-rails4 23 30: *rspec-metadata-mysql-rails4
+rspec-mysql-rails4 24 30: *rspec-metadata-mysql-rails4
+rspec-mysql-rails4 25 30: *rspec-metadata-mysql-rails4
+rspec-mysql-rails4 26 30: *rspec-metadata-mysql-rails4
+rspec-mysql-rails4 27 30: *rspec-metadata-mysql-rails4
+rspec-mysql-rails4 28 30: *rspec-metadata-mysql-rails4
+rspec-mysql-rails4 29 30: *rspec-metadata-mysql-rails4
static-analysis:
<<: *dedicated-no-docs-no-db-pull-cache-job
@@ -588,7 +593,7 @@ static-analysis:
script:
- scripts/static-analysis
cache:
- key: "ruby-2.4.5-debian-stretch-with-yarn-and-rubocop"
+ key: "debian-stretch-ruby-2.5.3-node-10.x-and-rubocop"
paths:
- vendor/ruby
- .yarn-cache/
@@ -624,14 +629,15 @@ downtime_check:
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
-rails5_gemfile_lock_check:
+rails4_gemfile_lock_check:
<<: *dedicated-no-docs-no-db-pull-cache-job
<<: *except-docs-and-qa
script:
- - scripts/rails5-gemfile-lock-check
+ - scripts/rails4-gemfile-lock-check
ee_compat_check:
<<: *rake-exec
+ dependencies: []
except:
- master
- tags
@@ -694,7 +700,10 @@ gitlab:setup-mysql:
# Frontend-related jobs
gitlab:assets:compile:
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
+ image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-git-2.18-chrome-69.0-node-8.x-yarn-1.2-graphicsmagick-1.3.29-docker-18.06.1
dependencies: []
+ services:
+ - docker:stable-dind
variables:
NODE_ENV: "production"
RAILS_ENV: "production"
@@ -703,18 +712,23 @@ gitlab:assets:compile:
WEBPACK_REPORT: "true"
# we override the max_old_space_size to prevent OOM errors
NODE_OPTIONS: --max_old_space_size=3584
+ DOCKER_DRIVER: overlay2
+ DOCKER_HOST: tcp://docker:2375
script:
- date
- yarn install --frozen-lockfile --production --cache-folder .yarn-cache
- date
- free -m
- bundle exec rake gitlab:assets:compile
+ - scripts/build_assets_image
artifacts:
name: webpack-report
expire_in: 31d
paths:
- webpack-report/
- public/assets/
+ tags:
+ - docker
karma:
<<: *dedicated-no-docs-pull-cache-job
@@ -766,7 +780,8 @@ code_quality:
--volume /var/run/docker.sock:/var/run/docker.sock
"registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
artifacts:
- paths: [gl-code-quality-report.json]
+ reports:
+ codequality: gl-code-quality-report.json
expire_in: 1 week
sast:
@@ -790,7 +805,8 @@ sast:
--volume /var/run/docker.sock:/var/run/docker.sock
"registry.gitlab.com/gitlab-org/security-products/sast:$SP_VERSION" /app/bin/run /code
artifacts:
- paths: [gl-sast-report.json]
+ reports:
+ sast: gl-sast-report.json
dependency_scanning:
<<: *dedicated-no-docs-no-db-pull-cache-job
@@ -812,7 +828,8 @@ dependency_scanning:
--volume /var/run/docker.sock:/var/run/docker.sock
"registry.gitlab.com/gitlab-org/security-products/dependency-scanning:$SP_VERSION" /code
artifacts:
- paths: [gl-dependency-scanning-report.json]
+ reports:
+ dependency_scanning: gl-dependency-scanning-report.json
qa:internal:
<<: *dedicated-no-docs-no-db-pull-cache-job
@@ -852,9 +869,7 @@ coverage:
lint:javascript:report:
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
stage: post-test
- dependencies:
- - compile-assets
- - setup-test-env
+ dependencies: []
before_script: []
script:
- date
@@ -908,6 +923,7 @@ gitlab_git_test:
variables:
SETUP_DB: "false"
before_script: []
+ dependencies: []
cache: {}
script:
- spec/support/prepare-gitlab-git-test-for-commit --check-for-changes
@@ -918,6 +934,7 @@ no_ee_check:
variables:
SETUP_DB: "false"
before_script: []
+ dependencies: []
cache: {}
script:
- scripts/no-ee-check
@@ -946,7 +963,6 @@ review:
- download_gitlab_chart
- ensure_namespace
- install_tiller
- - create_secret
- install_external_dns
- deploy
environment:
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS.disabled
index a4b773b15a9..a4b773b15a9 100644
--- a/.gitlab/CODEOWNERS
+++ b/.gitlab/CODEOWNERS.disabled
diff --git a/.nvmrc b/.nvmrc
index dba04c1e178..db24ab967fb 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-8.11.3
+10.13.0
diff --git a/.ruby-version b/.ruby-version
index 59aa62c1fa4..aedc15bb0c6 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-2.4.5
+2.5.3
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4c99f6ed059..6e5296e231e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,27 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 11.4.5 (2018-11-04)
+
+### Fixed (4 changes, 1 of them is from the community)
+
+- fix link to enable usage ping from convdev index. !22545 (Anand Capur)
+- Update gitlab-ui dependency to 1.8.0-hotfix.1 to fix IE11 bug.
+- Remove duplicate escape in job sidebar.
+- Fixed merge request fill tree toggling not respecting fluid width preference.
+
+### Other (1 change)
+
+- Fix stage dropdown not rendering in different languages.
+
+
+## 11.4.4 (2018-10-30)
+
+### Security (1 change)
+
+- Monkey kubeclient to not follow any redirects.
+
+
## 11.4.3 (2018-10-26)
- No changes.
@@ -250,6 +271,13 @@ entry.
- Check frozen string in style builds. (gfyoung)
+## 11.3.9 (2018-10-31)
+
+### Security (1 change)
+
+- Monkey kubeclient to not follow any redirects.
+
+
## 11.3.8 (2018-10-27)
- No changes.
@@ -555,6 +583,13 @@ entry.
- Creates Vue component for artifacts block on job page.
+## 11.2.8 (2018-10-31)
+
+### Security (1 change)
+
+- Monkey kubeclient to not follow any redirects.
+
+
## 11.2.7 (2018-10-27)
- No changes.
diff --git a/Dockerfile.assets b/Dockerfile.assets
new file mode 100644
index 00000000000..403d16cc4ab
--- /dev/null
+++ b/Dockerfile.assets
@@ -0,0 +1,4 @@
+# Simple container to store assets for later use
+FROM scratch
+ADD public/assets /assets/
+CMD /bin/true
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index bcc9c2840a7..4db8830b115 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.126.0
+0.129.0
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 4122521804f..21c8c7b46b8 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-7.0.0 \ No newline at end of file
+7.1.1
diff --git a/Gemfile b/Gemfile
index 6674edc1d0c..7030bd9c5e8 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,6 +1,6 @@
# --- Special code for migrating to Rails 5.0 ---
def rails5?
- %w[1 true].include?(ENV["RAILS5"])
+ !%w[0 false].include?(ENV["RAILS5"])
end
gem_versions = {}
@@ -124,7 +124,7 @@ gem 'seed-fu', '~> 2.3.7'
# Markdown and HTML processing
gem 'html-pipeline', '~> 2.8'
gem 'deckar01-task_list', '2.0.0'
-gem 'gitlab-markup', '~> 1.6.4'
+gem 'gitlab-markup', '~> 1.6.5'
gem 'github-markup', '~> 1.7.0', require: 'github/markup'
gem 'redcarpet', '~> 3.4'
gem 'commonmarker', '~> 0.17'
@@ -204,6 +204,9 @@ gem 'redis-rails', '~> 5.0.2'
gem 'redis', '~> 3.2'
gem 'connection_pool', '~> 2.0'
+# Discord integration
+gem 'discordrb-webhooks-blackst0ne', '~> 3.3', require: false
+
# HipChat integration
gem 'hipchat', '~> 1.5.0'
@@ -312,7 +315,7 @@ group :development do
# Better errors handler
gem 'better_errors', '~> 2.1.0'
- gem 'binding_of_caller', '~> 0.7.2'
+ gem 'binding_of_caller', '~> 0.8.0'
# thin instead webrick
gem 'thin', '~> 1.7.0'
@@ -339,7 +342,7 @@ group :development, :test do
gem 'minitest', '~> 5.7.0'
# Generate Fake data
- gem 'ffaker', '~> 2.4'
+ gem 'ffaker', '~> 2.10'
gem 'capybara', '~> 2.15'
gem 'capybara-screenshot', '~> 1.0.0'
@@ -354,7 +357,7 @@ group :development, :test do
gem 'rubocop-rspec', '~> 1.22.1'
gem 'scss_lint', '~> 0.56.0', require: false
- gem 'haml_lint', '~> 0.26.0', require: false
+ gem 'haml_lint', '~> 0.28.0', require: false
gem 'simplecov', '~> 0.14.0', require: false
gem 'bundler-audit', '~> 0.5.0', require: false
@@ -416,7 +419,7 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 0.118.1', require: 'gitaly'
+gem 'gitaly-proto', '~> 0.123.0', require: 'gitaly'
gem 'grpc', '~> 1.15.0'
gem 'google-protobuf', '~> 3.6'
@@ -431,6 +434,3 @@ gem 'flipper-active_support_cache_store', '~> 0.13.0'
# Structured logging
gem 'lograge', '~> 0.5'
gem 'grape_logging', '~> 1.7'
-
-# Asset synchronization
-gem 'asset_sync', '~> 2.4'
diff --git a/Gemfile.lock b/Gemfile.lock
index e755b0e0a8d..f08855ed049 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -4,41 +4,44 @@ GEM
RedCloth (4.3.2)
abstract_type (0.0.7)
ace-rails-ap (4.1.2)
- actionmailer (4.2.10)
- actionpack (= 4.2.10)
- actionview (= 4.2.10)
- activejob (= 4.2.10)
+ actioncable (5.0.7)
+ actionpack (= 5.0.7)
+ nio4r (>= 1.2, < 3.0)
+ websocket-driver (~> 0.6.1)
+ actionmailer (5.0.7)
+ actionpack (= 5.0.7)
+ actionview (= 5.0.7)
+ activejob (= 5.0.7)
mail (~> 2.5, >= 2.5.4)
- rails-dom-testing (~> 1.0, >= 1.0.5)
- actionpack (4.2.10)
- actionview (= 4.2.10)
- activesupport (= 4.2.10)
- rack (~> 1.6)
- rack-test (~> 0.6.2)
- rails-dom-testing (~> 1.0, >= 1.0.5)
+ rails-dom-testing (~> 2.0)
+ actionpack (5.0.7)
+ actionview (= 5.0.7)
+ activesupport (= 5.0.7)
+ rack (~> 2.0)
+ rack-test (~> 0.6.3)
+ rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
- actionview (4.2.10)
- activesupport (= 4.2.10)
+ actionview (5.0.7)
+ activesupport (= 5.0.7)
builder (~> 3.1)
erubis (~> 2.7.0)
- rails-dom-testing (~> 1.0, >= 1.0.5)
+ rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
- activejob (4.2.10)
- activesupport (= 4.2.10)
- globalid (>= 0.3.0)
- activemodel (4.2.10)
- activesupport (= 4.2.10)
- builder (~> 3.1)
- activerecord (4.2.10)
- activemodel (= 4.2.10)
- activesupport (= 4.2.10)
- arel (~> 6.0)
- activerecord_sane_schema_dumper (0.2)
- rails (>= 4, < 5)
- activesupport (4.2.10)
- i18n (~> 0.7)
+ activejob (5.0.7)
+ activesupport (= 5.0.7)
+ globalid (>= 0.3.6)
+ activemodel (5.0.7)
+ activesupport (= 5.0.7)
+ activerecord (5.0.7)
+ activemodel (= 5.0.7)
+ activesupport (= 5.0.7)
+ arel (~> 7.0)
+ activerecord_sane_schema_dumper (1.0)
+ rails (>= 5, < 6)
+ activesupport (5.0.7)
+ concurrent-ruby (~> 1.0, >= 1.0.2)
+ i18n (>= 0.7, < 2)
minitest (~> 5.1)
- thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
acts-as-taggable-on (5.0.0)
activerecord (>= 4.2.8)
@@ -49,7 +52,7 @@ GEM
public_suffix (>= 2.0.2, < 4.0)
aes_key_wrap (1.0.1)
akismet (2.0.0)
- arel (6.0.4)
+ arel (7.1.4)
asana (0.6.0)
faraday (~> 0.9)
faraday_middleware (~> 0.9)
@@ -58,11 +61,6 @@ GEM
asciidoctor (1.5.6.2)
asciidoctor-plantuml (0.0.8)
asciidoctor (~> 1.5)
- asset_sync (2.4.0)
- activemodel (>= 4.1.0)
- fog-core
- mime-types (>= 2.99)
- unf
ast (2.4.0)
atomic (1.1.99)
attr_encrypted (3.1.0)
@@ -84,7 +82,7 @@ GEM
erubis (>= 2.6.6)
rack (>= 0.9.0)
bindata (2.4.3)
- binding_of_caller (0.7.2)
+ binding_of_caller (0.8.0)
debug_inspector (>= 0.0.1)
bootsnap (1.3.2)
msgpack (~> 1.0)
@@ -142,14 +140,14 @@ GEM
addressable
daemons (1.2.6)
database_cleaner (1.5.3)
- debug_inspector (0.0.2)
+ debug_inspector (0.0.3)
debugger-ruby_core_source (1.3.8)
deckar01-task_list (2.0.0)
html-pipeline
declarative (0.0.10)
declarative-option (0.1.0)
- default_value_for (3.0.2)
- activerecord (>= 3.2.0, < 5.1)
+ default_value_for (3.0.5)
+ activerecord (>= 3.2.0, < 5.2)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
device_detector (1.0.0)
@@ -167,6 +165,8 @@ GEM
rotp (~> 2.0)
diff-lcs (1.3)
diffy (3.1.0)
+ discordrb-webhooks-blackst0ne (3.3.0)
+ rest-client (~> 2.0)
docile (1.1.5)
domain_name (0.5.20180417)
unf (>= 0.0.5, < 1.0.0)
@@ -205,7 +205,7 @@ GEM
multi_json
fast_blank (1.0.0)
fast_gettext (1.6.0)
- ffaker (2.4.0)
+ ffaker (2.10.0)
ffi (1.9.25)
flipper (0.13.0)
flipper-active_record (0.13.0)
@@ -274,11 +274,10 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
- gitaly-proto (0.118.1)
- google-protobuf (~> 3.1)
- grpc (~> 1.10)
+ gitaly-proto (0.123.0)
+ grpc (~> 1.0)
github-markup (1.7.0)
- gitlab-markup (1.6.4)
+ gitlab-markup (1.6.5)
gitlab-sidekiq-fetcher (0.3.0)
sidekiq (~> 5)
gitlab-styles (2.4.1)
@@ -341,11 +340,11 @@ GEM
haml (5.0.4)
temple (>= 0.8.0)
tilt
- haml_lint (0.26.0)
+ haml_lint (0.28.0)
haml (>= 4.0, < 5.1)
rainbow
rake (>= 10, < 13)
- rubocop (>= 0.49.0)
+ rubocop (>= 0.50.0)
sysexits (~> 1.1)
hamlit (2.8.8)
temple (>= 0.8.0)
@@ -380,7 +379,7 @@ GEM
json (~> 1.8)
multi_xml (>= 0.5.2)
httpclient (2.8.3)
- i18n (0.9.5)
+ i18n (1.1.0)
concurrent-ruby (~> 1.0)
icalendar (2.4.1)
ice_nine (0.11.2)
@@ -455,9 +454,9 @@ GEM
memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1)
method_source (0.9.0)
- mime-types (3.1)
+ mime-types (3.2.2)
mime-types-data (~> 3.2015)
- mime-types-data (3.2016.0521)
+ mime-types-data (3.2018.0812)
mimemagic (0.3.0)
mini_magick (4.8.0)
mini_mime (1.0.1)
@@ -474,6 +473,7 @@ GEM
net-ldap (0.16.0)
net-ssh (5.0.1)
netrc (0.11.0)
+ nio4r (2.3.1)
nokogiri (1.8.4)
mini_portile2 (~> 2.3.0)
nokogumbo (1.5.0)
@@ -602,7 +602,7 @@ GEM
get_process_mem (~> 0.2)
puma (>= 2.7, < 4)
pyu-ruby-sasl (0.0.3.3)
- rack (1.6.10)
+ rack (2.0.5)
rack-accept (0.4.5)
rack (>= 0.4)
rack-attack (4.4.1)
@@ -614,37 +614,42 @@ GEM
httpclient (>= 2.4)
multi_json (>= 1.3.6)
rack (>= 1.1)
- rack-protection (2.0.3)
+ rack-protection (2.0.4)
rack
rack-proxy (0.6.0)
rack
rack-test (0.6.3)
rack (>= 1.0)
- rails (4.2.10)
- actionmailer (= 4.2.10)
- actionpack (= 4.2.10)
- actionview (= 4.2.10)
- activejob (= 4.2.10)
- activemodel (= 4.2.10)
- activerecord (= 4.2.10)
- activesupport (= 4.2.10)
- bundler (>= 1.3.0, < 2.0)
- railties (= 4.2.10)
- sprockets-rails
+ rails (5.0.7)
+ actioncable (= 5.0.7)
+ actionmailer (= 5.0.7)
+ actionpack (= 5.0.7)
+ actionview (= 5.0.7)
+ activejob (= 5.0.7)
+ activemodel (= 5.0.7)
+ activerecord (= 5.0.7)
+ activesupport (= 5.0.7)
+ bundler (>= 1.3.0)
+ railties (= 5.0.7)
+ sprockets-rails (>= 2.0.0)
+ rails-controller-testing (1.0.2)
+ actionpack (~> 5.x, >= 5.0.1)
+ actionview (~> 5.x, >= 5.0.1)
+ activesupport (~> 5.x)
rails-deprecated_sanitizer (1.0.3)
activesupport (>= 4.2.0.alpha)
- rails-dom-testing (1.0.9)
- activesupport (>= 4.2.0, < 5.0)
- nokogiri (~> 1.6)
- rails-deprecated_sanitizer (>= 1.0.1)
+ rails-dom-testing (2.0.3)
+ activesupport (>= 4.2.0)
+ nokogiri (>= 1.6)
rails-html-sanitizer (1.0.4)
loofah (~> 2.2, >= 2.2.2)
- rails-i18n (4.0.9)
- i18n (~> 0.7)
- railties (~> 4.0)
- railties (4.2.10)
- actionpack (= 4.2.10)
- activesupport (= 4.2.10)
+ rails-i18n (5.1.1)
+ i18n (>= 0.7, < 2)
+ railties (>= 5.0, < 6)
+ railties (5.0.7)
+ actionpack (= 5.0.7)
+ activesupport (= 5.0.7)
+ method_source
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rainbow (3.0.0)
@@ -682,7 +687,7 @@ GEM
redis-actionpack (>= 5.0, < 6)
redis-activesupport (>= 5.0, < 6)
redis-store (>= 1.2, < 2)
- redis-store (1.4.1)
+ redis-store (1.6.0)
redis (>= 2.2, < 5)
regexp_parser (0.5.0)
representable (3.0.4)
@@ -735,8 +740,7 @@ GEM
rspec-core
rspec-set (0.1.3)
rspec-support (3.7.1)
- rspec_junit_formatter (0.2.3)
- builder (< 4)
+ rspec_junit_formatter (0.4.1)
rspec-core (>= 2, < 4, != 2.12.0)
rspec_profiling (0.0.5)
activerecord
@@ -808,7 +812,7 @@ GEM
rack
shoulda-matchers (3.1.2)
activesupport (>= 4.0.0)
- sidekiq (5.2.1)
+ sidekiq (5.2.3)
connection_pool (~> 2.2, >= 2.2.2)
rack-protection (>= 1.5.0)
redis (>= 3.3.5, < 5)
@@ -853,8 +857,6 @@ GEM
sysexits (1.2.0)
temple (0.8.0)
test-prof (0.2.5)
- test_after_commit (1.1.0)
- activerecord (>= 3.2)
text (1.3.1)
thin (1.7.2)
daemons (~> 1.0, >= 1.0.9)
@@ -917,6 +919,9 @@ GEM
hashdiff
webpack-rails (0.9.11)
railties (>= 3.2.0)
+ websocket-driver (0.6.5)
+ websocket-extensions (>= 0.1.0)
+ websocket-extensions (0.1.3)
wikicloth (0.8.1)
builder
expression_parser
@@ -932,14 +937,13 @@ PLATFORMS
DEPENDENCIES
RedCloth (~> 4.3.2)
ace-rails-ap (~> 4.1.0)
- activerecord_sane_schema_dumper (= 0.2)
+ activerecord_sane_schema_dumper (= 1.0)
acts-as-taggable-on (~> 5.0)
addressable (~> 2.5.2)
akismet (~> 2.0)
asana (~> 0.6.0)
asciidoctor (~> 1.5.6)
asciidoctor-plantuml (= 0.0.8)
- asset_sync (~> 2.4)
attr_encrypted (~> 3.1.0)
awesome_print
babosa (~> 1.0.2)
@@ -948,7 +952,7 @@ DEPENDENCIES
bcrypt_pbkdf (~> 1.0)
benchmark-ips (~> 2.3.0)
better_errors (~> 2.1.0)
- binding_of_caller (~> 0.7.2)
+ binding_of_caller (~> 0.8.0)
bootsnap (~> 1.3)
bootstrap_form (~> 2.7.0)
brakeman (~> 4.2)
@@ -967,11 +971,12 @@ DEPENDENCIES
creole (~> 0.5.0)
database_cleaner (~> 1.5.0)
deckar01-task_list (= 2.0.0)
- default_value_for (~> 3.0.0)
+ default_value_for (~> 3.0.5)
device_detector
devise (~> 4.4)
devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0)
+ discordrb-webhooks-blackst0ne (~> 3.3)
doorkeeper (~> 4.3)
doorkeeper-openid_connect (~> 1.5)
ed25519 (~> 1.2)
@@ -981,7 +986,7 @@ DEPENDENCIES
factory_bot_rails (~> 4.8.2)
faraday (~> 0.12)
fast_blank
- ffaker (~> 2.4)
+ ffaker (~> 2.10)
flipper (~> 0.13.0)
flipper-active_record (~> 0.13.0)
flipper-active_support_cache_store (~> 0.13.0)
@@ -1000,9 +1005,9 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 0.118.1)
+ gitaly-proto (~> 0.123.0)
github-markup (~> 1.7.0)
- gitlab-markup (~> 1.6.4)
+ gitlab-markup (~> 1.6.5)
gitlab-sidekiq-fetcher
gitlab-styles (~> 2.4)
gitlab_omniauth-ldap (~> 2.0.4)
@@ -1017,7 +1022,7 @@ DEPENDENCIES
graphiql-rails (~> 1.4.10)
graphql (~> 1.8.0)
grpc (~> 1.15.0)
- haml_lint (~> 0.26.0)
+ haml_lint (~> 0.28.0)
hamlit (~> 2.8.8)
hangouts-chat (~> 0.0.5)
hashie-forbidden_attributes
@@ -1084,9 +1089,10 @@ DEPENDENCIES
rack-cors (~> 1.0.0)
rack-oauth2 (~> 1.2.1)
rack-proxy (~> 0.6.0)
- rails (= 4.2.10)
+ rails (= 5.0.7)
+ rails-controller-testing
rails-deprecated_sanitizer (~> 1.0.3)
- rails-i18n (~> 4.0.9)
+ rails-i18n (~> 5.1)
rainbow (~> 3.0)
raindrops (~> 0.18)
rblineprof (~> 0.3.6)
@@ -1139,7 +1145,6 @@ DEPENDENCIES
state_machines-activerecord (~> 0.5.1)
sys-filesystem (~> 1.1.6)
test-prof (~> 0.2.5)
- test_after_commit (~> 1.1)
thin (~> 1.7.0)
timecop (~> 0.8.0)
toml-rb (~> 1.0.0)
@@ -1158,4 +1163,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.16.6
+ 1.17.1
diff --git a/Gemfile.rails5 b/Gemfile.rails4
index 2b526b19ba0..0ec00e702aa 100644
--- a/Gemfile.rails5
+++ b/Gemfile.rails4
@@ -1,6 +1,6 @@
-# BUNDLE_GEMFILE=Gemfile.rails5 bundle install
+# BUNDLE_GEMFILE=Gemfile.rails4 bundle install
-ENV["RAILS5"] = "true"
+ENV["RAILS5"] = "false"
gemfile = File.expand_path("../Gemfile", __FILE__)
diff --git a/Gemfile.rails5.lock b/Gemfile.rails4.lock
index 6ae7444f18f..5bec40de909 100644
--- a/Gemfile.rails5.lock
+++ b/Gemfile.rails4.lock
@@ -4,44 +4,41 @@ GEM
RedCloth (4.3.2)
abstract_type (0.0.7)
ace-rails-ap (4.1.2)
- actioncable (5.0.7)
- actionpack (= 5.0.7)
- nio4r (>= 1.2, < 3.0)
- websocket-driver (~> 0.6.1)
- actionmailer (5.0.7)
- actionpack (= 5.0.7)
- actionview (= 5.0.7)
- activejob (= 5.0.7)
+ actionmailer (4.2.10)
+ actionpack (= 4.2.10)
+ actionview (= 4.2.10)
+ activejob (= 4.2.10)
mail (~> 2.5, >= 2.5.4)
- rails-dom-testing (~> 2.0)
- actionpack (5.0.7)
- actionview (= 5.0.7)
- activesupport (= 5.0.7)
- rack (~> 2.0)
- rack-test (~> 0.6.3)
- rails-dom-testing (~> 2.0)
+ rails-dom-testing (~> 1.0, >= 1.0.5)
+ actionpack (4.2.10)
+ actionview (= 4.2.10)
+ activesupport (= 4.2.10)
+ rack (~> 1.6)
+ rack-test (~> 0.6.2)
+ rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
- actionview (5.0.7)
- activesupport (= 5.0.7)
+ actionview (4.2.10)
+ activesupport (= 4.2.10)
builder (~> 3.1)
erubis (~> 2.7.0)
- rails-dom-testing (~> 2.0)
+ rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
- activejob (5.0.7)
- activesupport (= 5.0.7)
- globalid (>= 0.3.6)
- activemodel (5.0.7)
- activesupport (= 5.0.7)
- activerecord (5.0.7)
- activemodel (= 5.0.7)
- activesupport (= 5.0.7)
- arel (~> 7.0)
- activerecord_sane_schema_dumper (1.0)
- rails (>= 5, < 6)
- activesupport (5.0.7)
- concurrent-ruby (~> 1.0, >= 1.0.2)
- i18n (>= 0.7, < 2)
+ activejob (4.2.10)
+ activesupport (= 4.2.10)
+ globalid (>= 0.3.0)
+ activemodel (4.2.10)
+ activesupport (= 4.2.10)
+ builder (~> 3.1)
+ activerecord (4.2.10)
+ activemodel (= 4.2.10)
+ activesupport (= 4.2.10)
+ arel (~> 6.0)
+ activerecord_sane_schema_dumper (0.2)
+ rails (>= 4, < 5)
+ activesupport (4.2.10)
+ i18n (~> 0.7)
minitest (~> 5.1)
+ thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
acts-as-taggable-on (5.0.0)
activerecord (>= 4.2.8)
@@ -52,7 +49,7 @@ GEM
public_suffix (>= 2.0.2, < 4.0)
aes_key_wrap (1.0.1)
akismet (2.0.0)
- arel (7.1.4)
+ arel (6.0.4)
asana (0.6.0)
faraday (~> 0.9)
faraday_middleware (~> 0.9)
@@ -61,11 +58,6 @@ GEM
asciidoctor (1.5.6.2)
asciidoctor-plantuml (0.0.8)
asciidoctor (~> 1.5)
- asset_sync (2.4.0)
- activemodel (>= 4.1.0)
- fog-core
- mime-types (>= 2.99)
- unf
ast (2.4.0)
atomic (1.1.99)
attr_encrypted (3.1.0)
@@ -87,7 +79,7 @@ GEM
erubis (>= 2.6.6)
rack (>= 0.9.0)
bindata (2.4.3)
- binding_of_caller (0.7.2)
+ binding_of_caller (0.8.0)
debug_inspector (>= 0.0.1)
bootsnap (1.3.2)
msgpack (~> 1.0)
@@ -145,14 +137,14 @@ GEM
addressable
daemons (1.2.6)
database_cleaner (1.5.3)
- debug_inspector (0.0.2)
+ debug_inspector (0.0.3)
debugger-ruby_core_source (1.3.8)
deckar01-task_list (2.0.0)
html-pipeline
declarative (0.0.10)
declarative-option (0.1.0)
- default_value_for (3.0.5)
- activerecord (>= 3.2.0, < 5.2)
+ default_value_for (3.0.2)
+ activerecord (>= 3.2.0, < 5.1)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
device_detector (1.0.0)
@@ -170,6 +162,8 @@ GEM
rotp (~> 2.0)
diff-lcs (1.3)
diffy (3.1.0)
+ discordrb-webhooks-blackst0ne (3.3.0)
+ rest-client (~> 2.0)
docile (1.1.5)
domain_name (0.5.20180417)
unf (>= 0.0.5, < 1.0.0)
@@ -208,7 +202,7 @@ GEM
multi_json
fast_blank (1.0.0)
fast_gettext (1.6.0)
- ffaker (2.4.0)
+ ffaker (2.10.0)
ffi (1.9.25)
flipper (0.13.0)
flipper-active_record (0.13.0)
@@ -277,11 +271,10 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
- gitaly-proto (0.118.1)
- google-protobuf (~> 3.1)
- grpc (~> 1.10)
+ gitaly-proto (0.123.0)
+ grpc (~> 1.0)
github-markup (1.7.0)
- gitlab-markup (1.6.4)
+ gitlab-markup (1.6.5)
gitlab-sidekiq-fetcher (0.3.0)
sidekiq (~> 5)
gitlab-styles (2.4.1)
@@ -344,11 +337,11 @@ GEM
haml (5.0.4)
temple (>= 0.8.0)
tilt
- haml_lint (0.26.0)
+ haml_lint (0.28.0)
haml (>= 4.0, < 5.1)
rainbow
rake (>= 10, < 13)
- rubocop (>= 0.49.0)
+ rubocop (>= 0.50.0)
sysexits (~> 1.1)
hamlit (2.8.8)
temple (>= 0.8.0)
@@ -383,7 +376,7 @@ GEM
json (~> 1.8)
multi_xml (>= 0.5.2)
httpclient (2.8.3)
- i18n (1.1.0)
+ i18n (0.9.5)
concurrent-ruby (~> 1.0)
icalendar (2.4.1)
ice_nine (0.11.2)
@@ -458,9 +451,9 @@ GEM
memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1)
method_source (0.9.0)
- mime-types (3.1)
+ mime-types (3.2.2)
mime-types-data (~> 3.2015)
- mime-types-data (3.2016.0521)
+ mime-types-data (3.2018.0812)
mimemagic (0.3.0)
mini_magick (4.8.0)
mini_mime (1.0.1)
@@ -477,7 +470,6 @@ GEM
net-ldap (0.16.0)
net-ssh (5.0.1)
netrc (0.11.0)
- nio4r (2.3.1)
nokogiri (1.8.4)
mini_portile2 (~> 2.3.0)
nokogumbo (1.5.0)
@@ -550,7 +542,7 @@ GEM
orm_adapter (0.5.0)
os (1.0.0)
parallel (1.12.1)
- parser (2.5.1.2)
+ parser (2.5.3.0)
ast (~> 2.4.0)
parslet (1.8.2)
peek (1.0.1)
@@ -606,7 +598,7 @@ GEM
get_process_mem (~> 0.2)
puma (>= 2.7, < 4)
pyu-ruby-sasl (0.0.3.3)
- rack (2.0.5)
+ rack (1.6.11)
rack-accept (0.4.5)
rack (>= 0.4)
rack-attack (4.4.1)
@@ -618,42 +610,37 @@ GEM
httpclient (>= 2.4)
multi_json (>= 1.3.6)
rack (>= 1.1)
- rack-protection (2.0.3)
+ rack-protection (2.0.4)
rack
rack-proxy (0.6.0)
rack
rack-test (0.6.3)
rack (>= 1.0)
- rails (5.0.7)
- actioncable (= 5.0.7)
- actionmailer (= 5.0.7)
- actionpack (= 5.0.7)
- actionview (= 5.0.7)
- activejob (= 5.0.7)
- activemodel (= 5.0.7)
- activerecord (= 5.0.7)
- activesupport (= 5.0.7)
- bundler (>= 1.3.0)
- railties (= 5.0.7)
- sprockets-rails (>= 2.0.0)
- rails-controller-testing (1.0.2)
- actionpack (~> 5.x, >= 5.0.1)
- actionview (~> 5.x, >= 5.0.1)
- activesupport (~> 5.x)
+ rails (4.2.10)
+ actionmailer (= 4.2.10)
+ actionpack (= 4.2.10)
+ actionview (= 4.2.10)
+ activejob (= 4.2.10)
+ activemodel (= 4.2.10)
+ activerecord (= 4.2.10)
+ activesupport (= 4.2.10)
+ bundler (>= 1.3.0, < 2.0)
+ railties (= 4.2.10)
+ sprockets-rails
rails-deprecated_sanitizer (1.0.3)
activesupport (>= 4.2.0.alpha)
- rails-dom-testing (2.0.3)
- activesupport (>= 4.2.0)
- nokogiri (>= 1.6)
+ rails-dom-testing (1.0.9)
+ activesupport (>= 4.2.0, < 5.0)
+ nokogiri (~> 1.6)
+ rails-deprecated_sanitizer (>= 1.0.1)
rails-html-sanitizer (1.0.4)
loofah (~> 2.2, >= 2.2.2)
- rails-i18n (5.1.1)
- i18n (>= 0.7, < 2)
- railties (>= 5.0, < 6)
- railties (5.0.7)
- actionpack (= 5.0.7)
- activesupport (= 5.0.7)
- method_source
+ rails-i18n (4.0.9)
+ i18n (~> 0.7)
+ railties (~> 4.0)
+ railties (4.2.10)
+ actionpack (= 4.2.10)
+ activesupport (= 4.2.10)
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rainbow (3.0.0)
@@ -691,7 +678,7 @@ GEM
redis-actionpack (>= 5.0, < 6)
redis-activesupport (>= 5.0, < 6)
redis-store (>= 1.2, < 2)
- redis-store (1.4.1)
+ redis-store (1.6.0)
redis (>= 2.2, < 5)
regexp_parser (0.5.0)
representable (3.0.4)
@@ -744,7 +731,8 @@ GEM
rspec-core
rspec-set (0.1.3)
rspec-support (3.7.1)
- rspec_junit_formatter (0.4.1)
+ rspec_junit_formatter (0.2.3)
+ builder (< 4)
rspec-core (>= 2, < 4, != 2.12.0)
rspec_profiling (0.0.5)
activerecord
@@ -816,7 +804,7 @@ GEM
rack
shoulda-matchers (3.1.2)
activesupport (>= 4.0.0)
- sidekiq (5.2.1)
+ sidekiq (5.2.3)
connection_pool (~> 2.2, >= 2.2.2)
rack-protection (>= 1.5.0)
redis (>= 3.3.5, < 5)
@@ -861,6 +849,8 @@ GEM
sysexits (1.2.0)
temple (0.8.0)
test-prof (0.2.5)
+ test_after_commit (1.1.0)
+ activerecord (>= 3.2)
text (1.3.1)
thin (1.7.2)
daemons (~> 1.0, >= 1.0.9)
@@ -923,9 +913,6 @@ GEM
hashdiff
webpack-rails (0.9.11)
railties (>= 3.2.0)
- websocket-driver (0.6.5)
- websocket-extensions (>= 0.1.0)
- websocket-extensions (0.1.3)
wikicloth (0.8.1)
builder
expression_parser
@@ -941,14 +928,13 @@ PLATFORMS
DEPENDENCIES
RedCloth (~> 4.3.2)
ace-rails-ap (~> 4.1.0)
- activerecord_sane_schema_dumper (= 1.0)
+ activerecord_sane_schema_dumper (= 0.2)
acts-as-taggable-on (~> 5.0)
addressable (~> 2.5.2)
akismet (~> 2.0)
asana (~> 0.6.0)
asciidoctor (~> 1.5.6)
asciidoctor-plantuml (= 0.0.8)
- asset_sync (~> 2.4)
attr_encrypted (~> 3.1.0)
awesome_print
babosa (~> 1.0.2)
@@ -957,7 +943,7 @@ DEPENDENCIES
bcrypt_pbkdf (~> 1.0)
benchmark-ips (~> 2.3.0)
better_errors (~> 2.1.0)
- binding_of_caller (~> 0.7.2)
+ binding_of_caller (~> 0.8.0)
bootsnap (~> 1.3)
bootstrap_form (~> 2.7.0)
brakeman (~> 4.2)
@@ -976,11 +962,12 @@ DEPENDENCIES
creole (~> 0.5.0)
database_cleaner (~> 1.5.0)
deckar01-task_list (= 2.0.0)
- default_value_for (~> 3.0.5)
+ default_value_for (~> 3.0.0)
device_detector
devise (~> 4.4)
devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0)
+ discordrb-webhooks-blackst0ne (~> 3.3)
doorkeeper (~> 4.3)
doorkeeper-openid_connect (~> 1.5)
ed25519 (~> 1.2)
@@ -990,7 +977,7 @@ DEPENDENCIES
factory_bot_rails (~> 4.8.2)
faraday (~> 0.12)
fast_blank
- ffaker (~> 2.4)
+ ffaker (~> 2.10)
flipper (~> 0.13.0)
flipper-active_record (~> 0.13.0)
flipper-active_support_cache_store (~> 0.13.0)
@@ -1009,9 +996,9 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 0.118.1)
+ gitaly-proto (~> 0.123.0)
github-markup (~> 1.7.0)
- gitlab-markup (~> 1.6.4)
+ gitlab-markup (~> 1.6.5)
gitlab-sidekiq-fetcher
gitlab-styles (~> 2.4)
gitlab_omniauth-ldap (~> 2.0.4)
@@ -1026,7 +1013,7 @@ DEPENDENCIES
graphiql-rails (~> 1.4.10)
graphql (~> 1.8.0)
grpc (~> 1.15.0)
- haml_lint (~> 0.26.0)
+ haml_lint (~> 0.28.0)
hamlit (~> 2.8.8)
hangouts-chat (~> 0.0.5)
hashie-forbidden_attributes
@@ -1093,10 +1080,9 @@ DEPENDENCIES
rack-cors (~> 1.0.0)
rack-oauth2 (~> 1.2.1)
rack-proxy (~> 0.6.0)
- rails (= 5.0.7)
- rails-controller-testing
+ rails (= 4.2.10)
rails-deprecated_sanitizer (~> 1.0.3)
- rails-i18n (~> 5.1)
+ rails-i18n (~> 4.0.9)
rainbow (~> 3.0)
raindrops (~> 0.18)
rblineprof (~> 0.3.6)
@@ -1149,6 +1135,7 @@ DEPENDENCIES
state_machines-activerecord (~> 0.5.1)
sys-filesystem (~> 1.1.6)
test-prof (~> 0.2.5)
+ test_after_commit (~> 1.1)
thin (~> 1.7.0)
timecop (~> 0.8.0)
toml-rb (~> 1.0.0)
@@ -1167,4 +1154,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.16.6
+ 1.17.1
diff --git a/app/assets/images/cluster_app_logos/knative.png b/app/assets/images/cluster_app_logos/knative.png
new file mode 100644
index 00000000000..0a2510c8549
--- /dev/null
+++ b/app/assets/images/cluster_app_logos/knative.png
Binary files differ
diff --git a/app/assets/javascripts/badges/components/badge.vue b/app/assets/javascripts/badges/components/badge.vue
index 97232d7f783..8512bf9dd7b 100644
--- a/app/assets/javascripts/badges/components/badge.vue
+++ b/app/assets/javascripts/badges/components/badge.vue
@@ -1,12 +1,14 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
import Tooltip from '~/vue_shared/directives/tooltip';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
name: 'Badge',
components: {
Icon,
Tooltip,
+ GlLoadingIcon,
},
directives: {
Tooltip,
diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue
index aff7c4180e3..47e6e618219 100644
--- a/app/assets/javascripts/badges/components/badge_form.vue
+++ b/app/assets/javascripts/badges/components/badge_form.vue
@@ -4,6 +4,7 @@ import { mapActions, mapState } from 'vuex';
import createFlash from '~/flash';
import { s__, sprintf } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import createEmptyBadge from '../empty_badge';
import Badge from './badge.vue';
@@ -14,6 +15,7 @@ export default {
components: {
Badge,
LoadingButton,
+ GlLoadingIcon,
},
props: {
isEditing: {
diff --git a/app/assets/javascripts/badges/components/badge_list.vue b/app/assets/javascripts/badges/components/badge_list.vue
index 359d3e10380..ab518820378 100644
--- a/app/assets/javascripts/badges/components/badge_list.vue
+++ b/app/assets/javascripts/badges/components/badge_list.vue
@@ -1,5 +1,6 @@
<script>
import { mapState } from 'vuex';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import BadgeListRow from './badge_list_row.vue';
import { GROUP_BADGE } from '../constants';
@@ -7,6 +8,7 @@ export default {
name: 'BadgeList',
components: {
BadgeListRow,
+ GlLoadingIcon,
},
computed: {
...mapState(['badges', 'isLoading', 'kind']),
diff --git a/app/assets/javascripts/badges/components/badge_list_row.vue b/app/assets/javascripts/badges/components/badge_list_row.vue
index 5d16ba3ce6d..f28eff18f03 100644
--- a/app/assets/javascripts/badges/components/badge_list_row.vue
+++ b/app/assets/javascripts/badges/components/badge_list_row.vue
@@ -2,6 +2,7 @@
import { mapActions, mapState } from 'vuex';
import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import { PROJECT_BADGE } from '../constants';
import Badge from './badge.vue';
@@ -10,6 +11,7 @@ export default {
components: {
Badge,
Icon,
+ GlLoadingIcon,
},
props: {
badge: {
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
index 3cc89ff1955..ec27ae8c291 100644
--- a/app/assets/javascripts/blob_edit/blob_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -13,7 +13,7 @@ export default () => {
if (editBlobForm.length) {
const urlRoot = editBlobForm.data('relativeUrlRoot');
const assetsPath = editBlobForm.data('assetsPrefix');
- const filePath = editBlobForm.data('blobFilename')
+ const filePath = editBlobForm.data('blobFilename');
const currentAction = $('.js-file-title').data('currentAction');
const projectId = editBlobForm.data('project-id');
diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js
index 623cda5679a..fb6e5291a61 100644
--- a/app/assets/javascripts/boards/components/board.js
+++ b/app/assets/javascripts/boards/components/board.js
@@ -42,7 +42,7 @@ export default Vue.extend({
required: true,
},
},
- data () {
+ data() {
return {
detailIssue: boardsStore.detail,
filter: boardsStore.filter,
@@ -55,27 +55,26 @@ export default Vue.extend({
},
isNewIssueShown() {
return this.list.type === 'backlog' || (!this.disabled && this.list.type !== 'closed');
- }
+ },
},
watch: {
filter: {
handler() {
this.list.page = 1;
- this.list.getIssues(true)
- .catch(() => {
- // TODO: handle request error
- });
+ this.list.getIssues(true).catch(() => {
+ // TODO: handle request error
+ });
},
deep: true,
- }
+ },
},
- mounted () {
+ mounted() {
this.sortableOptions = getBoardSortableDefaultOptions({
disabled: this.disabled,
group: 'boards',
draggable: '.is-draggable',
handle: '.js-board-handle',
- onEnd: (e) => {
+ onEnd: e => {
sortableEnd();
if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
@@ -86,14 +85,15 @@ export default Vue.extend({
boardsStore.moveList(list, order);
});
}
- }
+ },
});
this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions);
},
created() {
if (this.list.isExpandable && AccessorUtilities.isLocalStorageAccessSafe()) {
- const isCollapsed = localStorage.getItem(`boards.${this.boardId}.${this.list.type}.expanded`) === 'false';
+ const isCollapsed =
+ localStorage.getItem(`boards.${this.boardId}.${this.list.type}.expanded`) === 'false';
this.list.isExpanded = !isCollapsed;
}
@@ -107,7 +107,10 @@ export default Vue.extend({
this.list.isExpanded = !this.list.isExpanded;
if (AccessorUtilities.isLocalStorageAccessSafe()) {
- localStorage.setItem(`boards.${this.boardId}.${this.list.type}.expanded`, this.list.isExpanded);
+ localStorage.setItem(
+ `boards.${this.boardId}.${this.list.type}.expanded`,
+ this.list.isExpanded,
+ );
}
}
},
diff --git a/app/assets/javascripts/boards/components/board_blank_state.vue b/app/assets/javascripts/boards/components/board_blank_state.vue
index 38aaec73d7d..561a4636ef5 100644
--- a/app/assets/javascripts/boards/components/board_blank_state.vue
+++ b/app/assets/javascripts/boards/components/board_blank_state.vue
@@ -32,18 +32,18 @@ export default {
boardsStore.state.lists = _.sortBy(boardsStore.state.lists, 'position');
// Save the labels
- gl.boardService.generateDefaultLists()
+ gl.boardService
+ .generateDefaultLists()
.then(res => res.data)
- .then((data) => {
- data.forEach((listObj) => {
+ .then(data => {
+ data.forEach(listObj => {
const list = boardsStore.findList('title', listObj.title);
list.id = listObj.id;
list.label.id = listObj.label.id;
- list.getIssues()
- .catch(() => {
- // TODO: handle request error
- });
+ list.getIssues().catch(() => {
+ // TODO: handle request error
+ });
});
})
.catch(() => {
@@ -57,7 +57,6 @@ export default {
clearBlankState: boardsStore.removeBlankState.bind(boardsStore),
},
};
-
</script>
<template>
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index 843498f0d06..2f31316aa76 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -1,77 +1,77 @@
<script>
- /* eslint-disable vue/require-default-prop */
- import IssueCardInner from './issue_card_inner.vue';
- import eventHub from '../eventhub';
- import boardsStore from '../stores/boards_store';
+/* eslint-disable vue/require-default-prop */
+import IssueCardInner from './issue_card_inner.vue';
+import eventHub from '../eventhub';
+import boardsStore from '../stores/boards_store';
- export default {
- name: 'BoardsIssueCard',
- components: {
- IssueCardInner,
+export default {
+ name: 'BoardsIssueCard',
+ components: {
+ IssueCardInner,
+ },
+ props: {
+ list: {
+ type: Object,
+ default: () => ({}),
},
- props: {
- list: {
- type: Object,
- default: () => ({}),
- },
- issue: {
- type: Object,
- default: () => ({}),
- },
- issueLinkBase: {
- type: String,
- default: '',
- },
- disabled: {
- type: Boolean,
- default: false,
- },
- index: {
- type: Number,
- default: 0,
- },
- rootPath: {
- type: String,
- default: '',
- },
- groupId: {
- type: Number,
- },
+ issue: {
+ type: Object,
+ default: () => ({}),
},
- data() {
- return {
- showDetail: false,
- detailIssue: boardsStore.detail,
- };
+ issueLinkBase: {
+ type: String,
+ default: '',
},
- computed: {
- issueDetailVisible() {
- return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
- },
+ disabled: {
+ type: Boolean,
+ default: false,
},
- methods: {
- mouseDown() {
- this.showDetail = true;
- },
- mouseMove() {
- this.showDetail = false;
- },
- showIssue(e) {
- if (e.target.classList.contains('js-no-trigger')) return;
+ index: {
+ type: Number,
+ default: 0,
+ },
+ rootPath: {
+ type: String,
+ default: '',
+ },
+ groupId: {
+ type: Number,
+ },
+ },
+ data() {
+ return {
+ showDetail: false,
+ detailIssue: boardsStore.detail,
+ };
+ },
+ computed: {
+ issueDetailVisible() {
+ return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
+ },
+ },
+ methods: {
+ mouseDown() {
+ this.showDetail = true;
+ },
+ mouseMove() {
+ this.showDetail = false;
+ },
+ showIssue(e) {
+ if (e.target.classList.contains('js-no-trigger')) return;
- if (this.showDetail) {
- this.showDetail = false;
+ if (this.showDetail) {
+ this.showDetail = false;
- if (boardsStore.detail.issue && boardsStore.detail.issue.id === this.issue.id) {
- eventHub.$emit('clearDetailIssue');
- } else {
- eventHub.$emit('newDetailIssue', this.issue);
- boardsStore.detail.list = this.list;
- }
+ if (boardsStore.detail.issue && boardsStore.detail.issue.id === this.issue.id) {
+ eventHub.$emit('clearDetailIssue');
+ } else {
+ eventHub.$emit('newDetailIssue', this.issue);
+ boardsStore.detail.list = this.list;
}
- },
+ }
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 4dc56c670f0..5e28fc396ab 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -1,5 +1,6 @@
<script>
import Sortable from 'sortablejs';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import boardNewIssue from './board_new_issue.vue';
import boardCard from './board_card.vue';
import eventHub from '../eventhub';
@@ -11,6 +12,7 @@ export default {
components: {
boardCard,
boardNewIssue,
+ GlLoadingIcon,
},
props: {
groupId: {
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index ae2d1ee3c6e..ee3dc38bca6 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -62,7 +62,8 @@ export default {
eventHub.$emit(`scroll-board-list-${this.list.id}`);
this.cancel();
- return this.list.newIssue(issue)
+ return this.list
+ .newIssue(issue)
.then(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$refs.submitButton).enable();
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index 62666954de0..e637e1f1223 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -38,7 +38,7 @@ export default Vue.extend({
};
},
computed: {
- showSidebar () {
+ showSidebar() {
return Object.keys(this.issue).length;
},
milestoneTitle() {
@@ -51,18 +51,20 @@ export default Vue.extend({
return this.issue.labels && this.issue.labels.length;
},
labelDropdownTitle() {
- return this.hasLabels ? sprintf(__('%{firstLabel} +%{labelCount} more'), {
- firstLabel: this.issue.labels[0].title,
- labelCount: this.issue.labels.length - 1
- }) : __('Label');
+ return this.hasLabels
+ ? sprintf(__('%{firstLabel} +%{labelCount} more'), {
+ firstLabel: this.issue.labels[0].title,
+ labelCount: this.issue.labels.length - 1,
+ })
+ : __('Label');
},
selectedLabels() {
return this.hasLabels ? this.issue.labels.map(l => l.title).join(',') : '';
- }
+ },
},
watch: {
detail: {
- handler () {
+ handler() {
if (this.issue.id !== this.detail.issue.id) {
$('.block.assignee')
.find('input:not(.js-vue)[name="issue[assignee_ids][]"]')
@@ -71,17 +73,19 @@ export default Vue.extend({
});
$('.js-issue-board-sidebar', this.$el).each((i, el) => {
- $(el).data('glDropdown').clearMenu();
+ $(el)
+ .data('glDropdown')
+ .clearMenu();
});
}
this.issue = this.detail.issue;
this.list = this.detail.list;
},
- deep: true
+ deep: true,
},
},
- created () {
+ created() {
// Get events from glDropdown
eventHub.$on('sidebar.removeAssignee', this.removeAssignee);
eventHub.$on('sidebar.addAssignee', this.addAssignee);
@@ -94,7 +98,7 @@ export default Vue.extend({
eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees);
eventHub.$off('sidebar.saveAssignees', this.saveAssignees);
},
- mounted () {
+ mounted() {
new IssuableContext(this.currentUser);
new MilestoneSelect();
new DueDateSelectors();
@@ -102,29 +106,30 @@ export default Vue.extend({
new Sidebar();
},
methods: {
- closeSidebar () {
+ closeSidebar() {
this.detail.issue = {};
},
- assignSelf () {
+ assignSelf() {
// Notify gl dropdown that we are now assigning to current user
this.$refs.assigneeBlock.dispatchEvent(new Event('assignYourself'));
this.addAssignee(this.currentUser);
this.saveAssignees();
},
- removeAssignee (a) {
+ removeAssignee(a) {
boardsStore.detail.issue.removeAssignee(a);
},
- addAssignee (a) {
+ addAssignee(a) {
boardsStore.detail.issue.addAssignee(a);
},
- removeAllAssignees () {
+ removeAllAssignees() {
boardsStore.detail.issue.removeAllAssignees();
},
- saveAssignees () {
+ saveAssignees() {
this.loadingAssignees = true;
- boardsStore.detail.issue.update()
+ boardsStore.detail.issue
+ .update()
.then(() => {
this.loadingAssignees = false;
})
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue
index aa98f35786e..2315a48a306 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner.vue
@@ -1,164 +1,222 @@
<script>
- import $ from 'jquery';
- import Icon from '~/vue_shared/components/icon.vue';
- import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
- import eventHub from '../eventhub';
- import tooltip from '../../vue_shared/directives/tooltip';
- import boardsStore from '../stores/boards_store';
+import { GlTooltipDirective } from '@gitlab-org/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 UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+import eventHub from '../eventhub';
+import IssueDueDate from './issue_due_date.vue';
+import IssueTimeEstimate from './issue_time_estimate.vue';
+import boardsStore from '../stores/boards_store';
- export default {
- components: {
- UserAvatarLink,
- Icon,
- },
- directives: {
- tooltip,
- },
- props: {
- issue: {
- type: Object,
- required: true,
- },
- issueLinkBase: {
- type: String,
- required: true,
- },
- list: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- rootPath: {
- type: String,
- required: true,
- },
- updateFilters: {
- type: Boolean,
- required: false,
- default: false,
- },
- groupId: {
- type: Number,
- required: false,
- default: null,
- },
- },
- data() {
- return {
- limitBeforeCounter: 3,
- maxRender: 4,
- maxCounter: 99,
- };
+export default {
+ components: {
+ Icon,
+ UserAvatarLink,
+ TooltipOnTruncate,
+ IssueDueDate,
+ IssueTimeEstimate,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ issue: {
+ type: Object,
+ required: true,
},
- computed: {
- numberOverLimit() {
- return this.issue.assignees.length - this.limitBeforeCounter;
- },
- assigneeCounterTooltip() {
- return `${this.assigneeCounterLabel} more`;
- },
- assigneeCounterLabel() {
- if (this.numberOverLimit > this.maxCounter) {
- return `${this.maxCounter}+`;
- }
-
- return `+${this.numberOverLimit}`;
- },
- shouldRenderCounter() {
- if (this.issue.assignees.length <= this.maxRender) {
- return false;
- }
+ issueLinkBase: {
+ type: String,
+ required: true,
+ },
+ list: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ updateFilters: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ groupId: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ limitBeforeCounter: 2,
+ maxRender: 3,
+ maxCounter: 99,
+ };
+ },
+ computed: {
+ numberOverLimit() {
+ return this.issue.assignees.length - this.limitBeforeCounter;
+ },
+ assigneeCounterTooltip() {
+ const { numberOverLimit, maxCounter } = this;
+ const count = numberOverLimit > maxCounter ? maxCounter : numberOverLimit;
+ return sprintf(__('%{count} more assignees'), { count });
+ },
+ assigneeCounterLabel() {
+ if (this.numberOverLimit > this.maxCounter) {
+ return `${this.maxCounter}+`;
+ }
- return this.issue.assignees.length > this.numberOverLimit;
- },
- issueId() {
- if (this.issue.iid) {
- return `#${this.issue.iid}`;
- }
+ return `+${this.numberOverLimit}`;
+ },
+ shouldRenderCounter() {
+ if (this.issue.assignees.length <= this.maxRender) {
return false;
- },
- showLabelFooter() {
- return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
- },
- },
- methods: {
- isIndexLessThanlimit(index) {
- return index < this.limitBeforeCounter;
- },
- shouldRenderAssignee(index) {
- // Eg. maxRender is 4,
- // Render up to all 4 assignees if there are only 4 assigness
- // Otherwise render up to the limitBeforeCounter
- if (this.issue.assignees.length <= this.maxRender) {
- return index < this.maxRender;
- }
+ }
+
+ return this.issue.assignees.length > this.numberOverLimit;
+ },
+ issueId() {
+ if (this.issue.iid) {
+ return `#${this.issue.iid}`;
+ }
+ return false;
+ },
+ showLabelFooter() {
+ return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
+ },
+ issueReferencePath() {
+ const { referencePath, groupId } = this.issue;
+ return !groupId ? referencePath.split('#')[0] : null;
+ },
+ },
+ methods: {
+ isIndexLessThanlimit(index) {
+ return index < this.limitBeforeCounter;
+ },
+ shouldRenderAssignee(index) {
+ // Eg. maxRender is 4,
+ // Render up to all 4 assignees if there are only 4 assigness
+ // Otherwise render up to the limitBeforeCounter
+ if (this.issue.assignees.length <= this.maxRender) {
+ return index < this.maxRender;
+ }
+
+ return index < this.limitBeforeCounter;
+ },
+ assigneeUrl(assignee) {
+ if (!assignee) return '';
+ return `${this.rootPath}${assignee.username}`;
+ },
+ avatarUrlTitle(assignee) {
+ return `Avatar for ${assignee.name}`;
+ },
+ showLabel(label) {
+ if (!label.id) return false;
+ return true;
+ },
+ filterByLabel(label) {
+ if (!this.updateFilters) return;
+ const labelTitle = encodeURIComponent(label.title);
+ const filter = `label_name[]=${labelTitle}`;
+
+ this.applyFilter(filter);
+ },
+ filterByWeight(weight) {
+ if (!this.updateFilters) return;
- return index < this.limitBeforeCounter;
- },
- assigneeUrl(assignee) {
- return `${this.rootPath}${assignee.username}`;
- },
- assigneeUrlTitle(assignee) {
- return `Assigned to ${assignee.name}`;
- },
- avatarUrlTitle(assignee) {
- return `Avatar for ${assignee.name}`;
- },
- showLabel(label) {
- if (!label.id) return false;
- return true;
- },
- filterByLabel(label, e) {
- if (!this.updateFilters) return;
+ const issueWeight = encodeURIComponent(weight);
+ const filter = `weight=${issueWeight}`;
- const filterPath = boardsStore.filter.path.split('&');
- const labelTitle = encodeURIComponent(label.title);
- const param = `label_name[]=${labelTitle}`;
- const labelIndex = filterPath.indexOf(param);
- $(e.currentTarget).tooltip('hide');
+ this.applyFilter(filter);
+ },
+ applyFilter(filter) {
+ const filterPath = boardsStore.filter.path.split('&');
+ const filterIndex = filterPath.indexOf(filter);
- if (labelIndex === -1) {
- filterPath.push(param);
- } else {
- filterPath.splice(labelIndex, 1);
- }
+ if (filterIndex === -1) {
+ filterPath.push(filter);
+ } else {
+ filterPath.splice(filterIndex, 1);
+ }
- boardsStore.filter.path = filterPath.join('&');
+ boardsStore.filter.path = filterPath.join('&');
- boardsStore.updateFiltersUrl();
+ boardsStore.updateFiltersUrl();
- eventHub.$emit('updateTokens');
- },
- labelStyle(label) {
- return {
- backgroundColor: label.color,
- color: label.textColor,
- };
- },
- },
- };
+ eventHub.$emit('updateTokens');
+ },
+ labelStyle(label) {
+ return {
+ backgroundColor: label.color,
+ color: label.textColor,
+ };
+ },
+ },
+};
</script>
<template>
<div>
<div class="board-card-header">
- <h4 class="board-card-title">
+ <h4 class="board-card-title append-bottom-0 prepend-top-0">
<icon
v-if="issue.confidential"
+ v-gl-tooltip
name="eye-slash"
- class="confidential-icon"
- />
- <a
+ :title="__('Confidential')"
+ class="confidential-icon append-right-4"
+ :aria-label="__('Confidential')"
+ /><a
:href="issue.path"
:title="issue.title"
class="js-no-trigger"
@mousemove.stop>{{ issue.title }}</a>
+ </h4>
+ </div>
+ <div
+ v-if="showLabelFooter"
+ class="board-card-labels prepend-top-4 d-flex flex-wrap"
+ >
+ <button
+ v-for="label in issue.labels"
+ v-if="showLabel(label)"
+ :key="label.id"
+ v-gl-tooltip
+ :style="labelStyle(label)"
+ :title="label.description"
+ class="badge color-label append-right-4 prepend-top-4"
+ type="button"
+ @click="filterByLabel(label)"
+ >
+ {{ label.title }}
+ </button>
+ </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">
<span
- v-if="issueId"
- class="board-card-number append-right-5"
+ v-if="issue.referencePath"
+ class="board-card-number d-flex append-right-8 prepend-top-8"
>
- {{ issue.referencePath }}
+ <tooltip-on-truncate
+ v-if="issueReferencePath"
+ :title="issueReferencePath"
+ placement="bottom"
+ class="board-issue-path block-truncated bold"
+ >{{ issueReferencePath }}</tooltip-on-truncate>#{{ issue.iid }}
</span>
- </h4>
+ <span class="board-info-items prepend-top-8 d-inline-block">
+ <issue-due-date
+ v-if="issue.dueDate"
+ :date="issue.dueDate"
+ /><issue-time-estimate
+ v-if="issue.timeEstimate"
+ :estimate="issue.timeEstimate"
+ />
+ </span>
+ </div>
<div class="board-card-assignee">
<user-avatar-link
v-for="(assignee, index) in issue.assignees"
@@ -167,38 +225,26 @@
:link-href="assigneeUrl(assignee)"
:img-alt="avatarUrlTitle(assignee)"
:img-src="assignee.avatar"
- :tooltip-text="assigneeUrlTitle(assignee)"
+ :img-size="24"
class="js-no-trigger"
tooltip-placement="bottom"
- />
+ >
+ <span class="js-assignee-tooltip">
+ <span class="bold d-block">Assignee</span>
+ {{ assignee.name }}
+ <span class="text-white-50">@{{ assignee.username }}</span>
+ </span>
+ </user-avatar-link>
<span
v-if="shouldRenderCounter"
- v-tooltip
+ v-gl-tooltip
:title="assigneeCounterTooltip"
class="avatar-counter"
+ data-placement="bottom"
>
{{ assigneeCounterLabel }}
</span>
</div>
</div>
- <div
- v-if="showLabelFooter"
- class="board-card-footer"
- >
- <button
- v-for="label in issue.labels"
- v-if="showLabel(label)"
- :key="label.id"
- v-tooltip
- :style="labelStyle(label)"
- :title="label.description"
- class="badge color-label"
- type="button"
- data-container="body"
- @click="filterByLabel(label, $event)"
- >
- {{ label.title }}
- </button>
- </div>
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/issue_due_date.vue b/app/assets/javascripts/boards/components/issue_due_date.vue
new file mode 100644
index 00000000000..025ef7e9743
--- /dev/null
+++ b/app/assets/javascripts/boards/components/issue_due_date.vue
@@ -0,0 +1,90 @@
+<script>
+import dateFormat from 'dateformat';
+import { GlTooltip } from '@gitlab-org/gitlab-ui';
+import Icon from '~/vue_shared/components/icon.vue';
+import { __ } from '~/locale';
+import { getDayDifference, getTimeago, dateInWords } from '~/lib/utils/datetime_utility';
+
+export default {
+ components: {
+ Icon,
+ GlTooltip,
+ },
+ props: {
+ date: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ title() {
+ const timeago = getTimeago();
+ const { timeDifference, standardDateFormat } = this;
+ const formatedDate = standardDateFormat;
+
+ if (timeDifference >= -1 && timeDifference < 7) {
+ return `${timeago.format(this.issueDueDate)} (${formatedDate})`;
+ }
+
+ return timeago.format(this.issueDueDate);
+ },
+ body() {
+ const { timeDifference, issueDueDate, standardDateFormat } = this;
+
+ if (timeDifference === 0) {
+ return __('Today');
+ } else if (timeDifference === 1) {
+ return __('Tomorrow');
+ } else if (timeDifference === -1) {
+ return __('Yesterday');
+ } else if (timeDifference > 0 && timeDifference < 7) {
+ return dateFormat(issueDueDate, 'dddd', true);
+ }
+
+ return standardDateFormat;
+ },
+ issueDueDate() {
+ return new Date(this.date);
+ },
+ timeDifference() {
+ const today = new Date();
+ return getDayDifference(today, this.issueDueDate);
+ },
+ isPastDue() {
+ if (this.timeDifference >= 0) return false;
+ return true;
+ },
+ standardDateFormat() {
+ const today = new Date();
+ const isDueInCurrentYear = today.getFullYear() === this.issueDueDate.getFullYear();
+
+ return dateInWords(this.issueDueDate, true, isDueInCurrentYear);
+ },
+ },
+};
+</script>
+
+<template>
+ <span>
+ <span
+ ref="issueDueDate"
+ class="board-card-info card-number"
+ >
+ <icon
+ :class="{'text-danger': isPastDue, 'board-card-info-icon': true}"
+ name="calendar"
+ /><time
+ :class="{'text-danger': isPastDue}"
+ datetime="date"
+ class="board-card-info-text">{{ body }}</time>
+ </span>
+ <gl-tooltip
+ :target="() => $refs.issueDueDate"
+ placement="bottom"
+ >
+ <span class="bold">{{ __('Due date') }}</span>
+ <br />
+ <span :class="{'text-danger-muted': isPastDue}">{{ title }}</span>
+ </gl-tooltip>
+ </span>
+</template>
diff --git a/app/assets/javascripts/boards/components/issue_time_estimate.vue b/app/assets/javascripts/boards/components/issue_time_estimate.vue
new file mode 100644
index 00000000000..efc7daf7812
--- /dev/null
+++ b/app/assets/javascripts/boards/components/issue_time_estimate.vue
@@ -0,0 +1,48 @@
+<script>
+import { GlTooltip } from '@gitlab-org/gitlab-ui';
+import Icon from '~/vue_shared/components/icon.vue';
+import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
+
+export default {
+ components: {
+ Icon,
+ GlTooltip,
+ },
+ props: {
+ estimate: {
+ type: Number,
+ required: true,
+ },
+ },
+ computed: {
+ title() {
+ return stringifyTime(parseSeconds(this.estimate), true);
+ },
+ timeEstimate() {
+ return stringifyTime(parseSeconds(this.estimate));
+ },
+ },
+};
+</script>
+
+<template>
+ <span>
+ <span
+ ref="issueTimeEstimate"
+ class="board-card-info card-number"
+ >
+ <icon
+ name="hourglass"
+ css-classes="board-card-info-icon"
+ /><time class="board-card-info-text">{{ timeEstimate }}</time>
+ </span>
+ <gl-tooltip
+ :target="() => $refs.issueTimeEstimate"
+ placement="bottom"
+ class="js-issue-time-estimate"
+ >
+ <span class="bold d-block">{{ __('Time estimate') }}</span>
+ {{ title }}
+ </gl-tooltip>
+ </span>
+</template>
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.vue b/app/assets/javascripts/boards/components/modal/empty_state.vue
index dbd69f84526..795ba864545 100644
--- a/app/assets/javascripts/boards/components/modal/empty_state.vue
+++ b/app/assets/javascripts/boards/components/modal/empty_state.vue
@@ -20,7 +20,7 @@ export default {
computed: {
contents() {
const obj = {
- title: 'You haven\'t added any issues to your project yet',
+ title: "You haven't added any issues to your project yet",
content: `
An issue can be a bug, a todo or a feature request that needs to be
discussed in a project. Besides, issues are searchable and filterable.
@@ -28,7 +28,7 @@ export default {
};
if (this.activeTab === 'selected') {
- obj.title = 'You haven\'t selected any issues yet';
+ obj.title = "You haven't selected any issues yet";
obj.content = `
Go back to <strong>Open issues</strong> and select some issues
to add to your board.
diff --git a/app/assets/javascripts/boards/components/modal/footer.vue b/app/assets/javascripts/boards/components/modal/footer.vue
index 268ca6bca13..d51597ed22d 100644
--- a/app/assets/javascripts/boards/components/modal/footer.vue
+++ b/app/assets/javascripts/boards/components/modal/footer.vue
@@ -42,19 +42,17 @@ export default {
const req = this.buildUpdateRequest(list);
// Post the data to the backend
- gl.boardService
- .bulkUpdate(issueIds, req)
- .catch(() => {
- Flash(__('Failed to update issues, please try again.'));
+ gl.boardService.bulkUpdate(issueIds, req).catch(() => {
+ Flash(__('Failed to update issues, please try again.'));
- selectedIssues.forEach((issue) => {
- list.removeIssue(issue);
- list.issuesSize -= 1;
- });
+ selectedIssues.forEach(issue => {
+ list.removeIssue(issue);
+ list.issuesSize -= 1;
});
+ });
// Add the issues on the frontend
- selectedIssues.forEach((issue) => {
+ selectedIssues.forEach(issue => {
list.addIssue(issue);
list.issuesSize += 1;
});
diff --git a/app/assets/javascripts/boards/components/modal/header.vue b/app/assets/javascripts/boards/components/modal/header.vue
index 979fb4d7199..fc6cefa89a9 100644
--- a/app/assets/javascripts/boards/components/modal/header.vue
+++ b/app/assets/javascripts/boards/components/modal/header.vue
@@ -1,52 +1,52 @@
<script>
- import ModalFilters from './filters';
- import ModalTabs from './tabs.vue';
- import ModalStore from '../../stores/modal_store';
- import modalMixin from '../../mixins/modal_mixins';
+import ModalFilters from './filters';
+import ModalTabs from './tabs.vue';
+import ModalStore from '../../stores/modal_store';
+import modalMixin from '../../mixins/modal_mixins';
- export default {
- components: {
- ModalTabs,
- ModalFilters,
+export default {
+ components: {
+ ModalTabs,
+ ModalFilters,
+ },
+ mixins: [modalMixin],
+ props: {
+ projectId: {
+ type: Number,
+ required: true,
},
- mixins: [modalMixin],
- props: {
- projectId: {
- type: Number,
- required: true,
- },
- milestonePath: {
- type: String,
- required: true,
- },
- labelPath: {
- type: String,
- required: true,
- },
+ milestonePath: {
+ type: String,
+ required: true,
},
- data() {
- return ModalStore.store;
+ labelPath: {
+ type: String,
+ required: true,
},
- computed: {
- selectAllText() {
- if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
- return 'Select all';
- }
+ },
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ selectAllText() {
+ if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
+ return 'Select all';
+ }
- return 'Deselect all';
- },
- showSearch() {
- return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
- },
+ return 'Deselect all';
},
- methods: {
- toggleAll() {
- this.$refs.selectAllBtn.blur();
+ showSearch() {
+ return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
+ },
+ },
+ methods: {
+ toggleAll() {
+ this.$refs.selectAllBtn.blur();
- ModalStore.toggleAll();
- },
+ ModalStore.toggleAll();
},
- };
+ },
+};
</script>
<template>
<div>
diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue
index 0c4c709324d..fdd1346d4c7 100644
--- a/app/assets/javascripts/boards/components/modal/index.vue
+++ b/app/assets/javascripts/boards/components/modal/index.vue
@@ -1,143 +1,146 @@
<script>
- /* global ListIssue */
- import { urlParamsToObject } from '~/lib/utils/common_utils';
- import ModalHeader from './header.vue';
- import ModalList from './list.vue';
- import ModalFooter from './footer.vue';
- import EmptyState from './empty_state.vue';
- import ModalStore from '../../stores/modal_store';
+/* global ListIssue */
+import { urlParamsToObject } from '~/lib/utils/common_utils';
+import ModalHeader from './header.vue';
+import ModalList from './list.vue';
+import ModalFooter from './footer.vue';
+import EmptyState from './empty_state.vue';
+import ModalStore from '../../stores/modal_store';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
- export default {
- components: {
- EmptyState,
- ModalHeader,
- ModalList,
- ModalFooter,
+export default {
+ components: {
+ EmptyState,
+ ModalHeader,
+ ModalList,
+ ModalFooter,
+ GlLoadingIcon,
+ },
+ props: {
+ newIssuePath: {
+ type: String,
+ required: true,
},
- props: {
- newIssuePath: {
- type: String,
- required: true,
- },
- emptyStateSvg: {
- type: String,
- required: true,
- },
- issueLinkBase: {
- type: String,
- required: true,
- },
- rootPath: {
- type: String,
- required: true,
- },
- projectId: {
- type: Number,
- required: true,
- },
- milestonePath: {
- type: String,
- required: true,
- },
- labelPath: {
- type: String,
- required: true,
- },
+ emptyStateSvg: {
+ type: String,
+ required: true,
},
- data() {
- return ModalStore.store;
+ issueLinkBase: {
+ type: String,
+ required: true,
},
- computed: {
- showList() {
- if (this.activeTab === 'selected') {
- return this.selectedIssues.length > 0;
- }
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ projectId: {
+ type: Number,
+ required: true,
+ },
+ milestonePath: {
+ type: String,
+ required: true,
+ },
+ labelPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ showList() {
+ if (this.activeTab === 'selected') {
+ return this.selectedIssues.length > 0;
+ }
- return this.issuesCount > 0;
- },
- showEmptyState() {
- if (!this.loading && this.issuesCount === 0) {
- return true;
- }
+ return this.issuesCount > 0;
+ },
+ showEmptyState() {
+ if (!this.loading && this.issuesCount === 0) {
+ return true;
+ }
- return this.activeTab === 'selected' && this.selectedIssues.length === 0;
- },
+ return this.activeTab === 'selected' && this.selectedIssues.length === 0;
},
- watch: {
- page() {
- this.loadIssues();
- },
- showAddIssuesModal() {
- if (this.showAddIssuesModal && !this.issues.length) {
- this.loading = true;
+ },
+ watch: {
+ page() {
+ this.loadIssues();
+ },
+ showAddIssuesModal() {
+ if (this.showAddIssuesModal && !this.issues.length) {
+ this.loading = true;
+ const loadingDone = () => {
+ this.loading = false;
+ };
+
+ this.loadIssues()
+ .then(loadingDone)
+ .catch(loadingDone);
+ } else if (!this.showAddIssuesModal) {
+ this.issues = [];
+ this.selectedIssues = [];
+ this.issuesCount = false;
+ }
+ },
+ filter: {
+ handler() {
+ if (this.$el.tagName) {
+ this.page = 1;
+ this.filterLoading = true;
const loadingDone = () => {
- this.loading = false;
+ this.filterLoading = false;
};
- this.loadIssues()
+ this.loadIssues(true)
.then(loadingDone)
.catch(loadingDone);
- } else if (!this.showAddIssuesModal) {
- this.issues = [];
- this.selectedIssues = [];
- this.issuesCount = false;
}
},
- filter: {
- handler() {
- if (this.$el.tagName) {
- this.page = 1;
- this.filterLoading = true;
- const loadingDone = () => {
- this.filterLoading = false;
- };
-
- this.loadIssues(true)
- .then(loadingDone)
- .catch(loadingDone);
- }
- },
- deep: true,
- },
+ deep: true,
},
- created() {
- this.page = 1;
- },
- methods: {
- loadIssues(clearIssues = false) {
- if (!this.showAddIssuesModal) return false;
+ },
+ created() {
+ this.page = 1;
+ },
+ methods: {
+ loadIssues(clearIssues = false) {
+ if (!this.showAddIssuesModal) return false;
- return gl.boardService.getBacklog({
+ return gl.boardService
+ .getBacklog({
...urlParamsToObject(this.filter.path),
page: this.page,
per: this.perPage,
})
- .then(res => res.data)
- .then(data => {
- if (clearIssues) {
- this.issues = [];
- }
+ .then(res => res.data)
+ .then(data => {
+ if (clearIssues) {
+ this.issues = [];
+ }
- data.issues.forEach(issueObj => {
- const issue = new ListIssue(issueObj);
- const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
- issue.selected = !!foundSelectedIssue;
+ data.issues.forEach(issueObj => {
+ const issue = new ListIssue(issueObj);
+ const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
+ issue.selected = !!foundSelectedIssue;
- this.issues.push(issue);
- });
+ this.issues.push(issue);
+ });
- this.loadingNewPage = false;
+ this.loadingNewPage = false;
- if (!this.issuesCount) {
- this.issuesCount = data.size;
- }
- })
- .catch(() => {
- // TODO: handle request error
- });
- },
+ if (!this.issuesCount) {
+ this.issuesCount = data.size;
+ }
+ })
+ .catch(() => {
+ // TODO: handle request error
+ });
},
- };
+ },
+};
</script>
<template>
<div
diff --git a/app/assets/javascripts/boards/components/modal/list.vue b/app/assets/javascripts/boards/components/modal/list.vue
index c93fd9f415c..e11f398e70d 100644
--- a/app/assets/javascripts/boards/components/modal/list.vue
+++ b/app/assets/javascripts/boards/components/modal/list.vue
@@ -1,120 +1,120 @@
<script>
- import Icon from '~/vue_shared/components/icon.vue';
- import bp from '../../../breakpoints';
- import ModalStore from '../../stores/modal_store';
- import IssueCardInner from '../issue_card_inner.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import bp from '../../../breakpoints';
+import ModalStore from '../../stores/modal_store';
+import IssueCardInner from '../issue_card_inner.vue';
- export default {
- components: {
- IssueCardInner,
- Icon,
+export default {
+ components: {
+ IssueCardInner,
+ Icon,
+ },
+ props: {
+ issueLinkBase: {
+ type: String,
+ required: true,
},
- props: {
- issueLinkBase: {
- type: String,
- required: true,
- },
- rootPath: {
- type: String,
- required: true,
- },
- emptyStateSvg: {
- type: String,
- required: true,
- },
+ rootPath: {
+ type: String,
+ required: true,
},
- data() {
- return ModalStore.store;
+ emptyStateSvg: {
+ type: String,
+ required: true,
},
- computed: {
- loopIssues() {
- if (this.activeTab === 'all') {
- return this.issues;
- }
+ },
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ loopIssues() {
+ if (this.activeTab === 'all') {
+ return this.issues;
+ }
- return this.selectedIssues;
- },
- groupedIssues() {
- const groups = [];
- this.loopIssues.forEach((issue, i) => {
- const index = i % this.columns;
+ return this.selectedIssues;
+ },
+ groupedIssues() {
+ const groups = [];
+ this.loopIssues.forEach((issue, i) => {
+ const index = i % this.columns;
- if (!groups[index]) {
- groups.push([]);
- }
+ if (!groups[index]) {
+ groups.push([]);
+ }
- groups[index].push(issue);
- });
+ groups[index].push(issue);
+ });
- return groups;
- },
+ return groups;
},
- watch: {
- activeTab() {
- if (this.activeTab === 'all') {
- ModalStore.purgeUnselectedIssues();
- }
- },
+ },
+ watch: {
+ activeTab() {
+ if (this.activeTab === 'all') {
+ ModalStore.purgeUnselectedIssues();
+ }
},
- mounted() {
- this.scrollHandlerWrapper = this.scrollHandler.bind(this);
- this.setColumnCountWrapper = this.setColumnCount.bind(this);
- this.setColumnCount();
+ },
+ mounted() {
+ this.scrollHandlerWrapper = this.scrollHandler.bind(this);
+ this.setColumnCountWrapper = this.setColumnCount.bind(this);
+ this.setColumnCount();
- this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper);
- window.addEventListener('resize', this.setColumnCountWrapper);
+ this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper);
+ window.addEventListener('resize', this.setColumnCountWrapper);
+ },
+ beforeDestroy() {
+ this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper);
+ window.removeEventListener('resize', this.setColumnCountWrapper);
+ },
+ methods: {
+ scrollHandler() {
+ const currentPage = Math.floor(this.issues.length / this.perPage);
+
+ if (
+ this.scrollTop() > this.scrollHeight() - 100 &&
+ !this.loadingNewPage &&
+ currentPage === this.page
+ ) {
+ this.loadingNewPage = true;
+ this.page += 1;
+ }
},
- beforeDestroy() {
- this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper);
- window.removeEventListener('resize', this.setColumnCountWrapper);
+ toggleIssue(e, issue) {
+ if (e.target.tagName !== 'A') {
+ ModalStore.toggleIssue(issue);
+ }
},
- methods: {
- scrollHandler() {
- const currentPage = Math.floor(this.issues.length / this.perPage);
-
- if (
- this.scrollTop() > this.scrollHeight() - 100 &&
- !this.loadingNewPage &&
- currentPage === this.page
- ) {
- this.loadingNewPage = true;
- this.page += 1;
- }
- },
- toggleIssue(e, issue) {
- if (e.target.tagName !== 'A') {
- ModalStore.toggleIssue(issue);
- }
- },
- listHeight() {
- return this.$refs.list.getBoundingClientRect().height;
- },
- scrollHeight() {
- return this.$refs.list.scrollHeight;
- },
- scrollTop() {
- return this.$refs.list.scrollTop + this.listHeight();
- },
- showIssue(issue) {
- if (this.activeTab === 'all') return true;
+ listHeight() {
+ return this.$refs.list.getBoundingClientRect().height;
+ },
+ scrollHeight() {
+ return this.$refs.list.scrollHeight;
+ },
+ scrollTop() {
+ return this.$refs.list.scrollTop + this.listHeight();
+ },
+ showIssue(issue) {
+ if (this.activeTab === 'all') return true;
- const index = ModalStore.selectedIssueIndex(issue);
+ const index = ModalStore.selectedIssueIndex(issue);
- return index !== -1;
- },
- setColumnCount() {
- const breakpoint = bp.getBreakpointSize();
+ return index !== -1;
+ },
+ setColumnCount() {
+ const breakpoint = bp.getBreakpointSize();
- if (breakpoint === 'lg' || breakpoint === 'md') {
- this.columns = 3;
- } else if (breakpoint === 'sm') {
- this.columns = 2;
- } else {
- this.columns = 1;
- }
- },
+ if (breakpoint === 'lg' || breakpoint === 'md') {
+ this.columns = 3;
+ } else if (breakpoint === 'sm') {
+ this.columns = 2;
+ } else {
+ this.columns = 1;
+ }
},
- };
+ },
+};
</script>
<template>
<section
diff --git a/app/assets/javascripts/boards/components/modal/tabs.vue b/app/assets/javascripts/boards/components/modal/tabs.vue
index d926b080094..5d661590e8e 100644
--- a/app/assets/javascripts/boards/components/modal/tabs.vue
+++ b/app/assets/javascripts/boards/components/modal/tabs.vue
@@ -1,21 +1,21 @@
<script>
- import ModalStore from '../../stores/modal_store';
- import modalMixin from '../../mixins/modal_mixins';
+import ModalStore from '../../stores/modal_store';
+import modalMixin from '../../mixins/modal_mixins';
- export default {
- mixins: [modalMixin],
- data() {
- return ModalStore.store;
+export default {
+ mixins: [modalMixin],
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ selectedCount() {
+ return ModalStore.selectedCount();
},
- computed: {
- selectedCount() {
- return ModalStore.selectedCount();
- },
- },
- destroyed() {
- this.activeTab = 'all';
- },
- };
+ },
+ destroyed() {
+ this.activeTab = 'all';
+ },
+};
</script>
<template>
<div class="top-area prepend-top-10 append-bottom-10">
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js
index 2c2045f8901..f7016561f93 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js
@@ -6,36 +6,41 @@ import _ from 'underscore';
import CreateLabelDropdown from '../../create_label';
import boardsStore from '../stores/boards_store';
-$(document).off('created.label').on('created.label', (e, label) => {
- boardsStore.new({
- title: label.title,
- position: boardsStore.state.lists.length - 2,
- list_type: 'label',
- label: {
- id: label.id,
+$(document)
+ .off('created.label')
+ .on('created.label', (e, label) => {
+ boardsStore.new({
title: label.title,
- color: label.color,
- },
+ position: boardsStore.state.lists.length - 2,
+ list_type: 'label',
+ label: {
+ id: label.id,
+ title: label.title,
+ color: label.color,
+ },
+ });
});
-});
export default function initNewListDropdown() {
- $('.js-new-board-list').each(function () {
+ $('.js-new-board-list').each(function() {
const $this = $(this);
- new CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespacePath'), $this.data('projectPath'));
+ new CreateLabelDropdown(
+ $this.closest('.dropdown').find('.dropdown-new-label'),
+ $this.data('namespacePath'),
+ $this.data('projectPath'),
+ );
$this.glDropdown({
data(term, callback) {
- axios.get($this.attr('data-list-labels-path'))
- .then(({ data }) => {
- callback(data);
- });
+ axios.get($this.attr('data-list-labels-path')).then(({ data }) => {
+ callback(data);
+ });
},
- renderRow (label) {
+ renderRow(label) {
const active = boardsStore.findList('title', label.title);
const $li = $('<li />');
const $a = $('<a />', {
- class: (active ? `is-active js-board-list-${active.id}` : ''),
+ class: active ? `is-active js-board-list-${active.id}` : '',
text: label.title,
href: '#',
});
@@ -53,7 +58,7 @@ export default function initNewListDropdown() {
selectable: true,
multiSelect: true,
containerSelector: '.js-tab-container-labels .dropdown-page-one .dropdown-content',
- clicked (options) {
+ clicked(options) {
const { e } = options;
const label = options.selectedObj;
e.preventDefault();
diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue
index 427a0868b0c..503417644fa 100644
--- a/app/assets/javascripts/boards/components/project_select.vue
+++ b/app/assets/javascripts/boards/components/project_select.vue
@@ -2,6 +2,7 @@
import $ from 'jquery';
import _ from 'underscore';
import Icon from '~/vue_shared/components/icon.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import eventHub from '../eventhub';
import Api from '../../api';
@@ -9,6 +10,7 @@ export default {
name: 'BoardProjectSelect',
components: {
Icon,
+ GlLoadingIcon,
},
props: {
groupId: {
@@ -46,7 +48,7 @@ export default {
selectable: true,
data: (term, callback) => {
this.loading = true;
- return Api.groupProjects(this.groupId, term, {with_issues_enabled: true}, projects => {
+ return Api.groupProjects(this.groupId, term, { with_issues_enabled: true }, projects => {
this.loading = false;
callback(projects);
});
@@ -54,7 +56,9 @@ export default {
renderRow(project) {
return `
<li>
- <a href='#' class='dropdown-menu-link' data-project-id="${project.id}" data-project-name="${project.name}">
+ <a href='#' class='dropdown-menu-link' data-project-id="${
+ project.id
+ }" data-project-name="${project.name}">
${_.escape(project.name)}
</a>
</li>
diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
index b8f2e324d43..d681e6a431c 100644
--- a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
+++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
@@ -1,79 +1,77 @@
<script>
- import Vue from 'vue';
- import Flash from '../../../flash';
- import { __ } from '../../../locale';
- import boardsStore from '../../stores/boards_store';
+import Vue from 'vue';
+import Flash from '../../../flash';
+import { __ } from '../../../locale';
+import boardsStore from '../../stores/boards_store';
- export default Vue.extend({
- props: {
- issue: {
- type: Object,
- required: true,
- },
- list: {
- type: Object,
- required: true,
- },
+export default Vue.extend({
+ props: {
+ issue: {
+ type: Object,
+ required: true,
},
- computed: {
- updateUrl() {
- return this.issue.path;
- },
+ list: {
+ type: Object,
+ required: true,
},
- methods: {
- removeIssue() {
- const { issue } = this;
- const lists = issue.getLists();
- const req = this.buildPatchRequest(issue, lists);
-
- const data = {
- issue: this.seedPatchRequest(issue, req),
- };
+ },
+ computed: {
+ updateUrl() {
+ return this.issue.path;
+ },
+ },
+ methods: {
+ removeIssue() {
+ const { issue } = this;
+ const lists = issue.getLists();
+ const req = this.buildPatchRequest(issue, lists);
- if (data.issue.label_ids.length === 0) {
- data.issue.label_ids = [''];
- }
+ const data = {
+ issue: this.seedPatchRequest(issue, req),
+ };
- // Post the remove data
- Vue.http.patch(this.updateUrl, data).catch(() => {
- Flash(__('Failed to remove issue from board, please try again.'));
+ if (data.issue.label_ids.length === 0) {
+ data.issue.label_ids = [''];
+ }
- lists.forEach(list => {
- list.addIssue(issue);
- });
- });
+ // Post the remove data
+ Vue.http.patch(this.updateUrl, data).catch(() => {
+ Flash(__('Failed to remove issue from board, please try again.'));
- // Remove from the frontend store
lists.forEach(list => {
- list.removeIssue(issue);
+ list.addIssue(issue);
});
+ });
- boardsStore.detail.issue = {};
- },
- /**
- * Build the default patch request.
- */
- buildPatchRequest(issue, lists) {
- const listLabelIds = lists.map(list => list.label.id);
+ // Remove from the frontend store
+ lists.forEach(list => {
+ list.removeIssue(issue);
+ });
- const labelIds = issue.labels
- .map(label => label.id)
- .filter(id => !listLabelIds.includes(id));
+ boardsStore.detail.issue = {};
+ },
+ /**
+ * Build the default patch request.
+ */
+ buildPatchRequest(issue, lists) {
+ const listLabelIds = lists.map(list => list.label.id);
+
+ const labelIds = issue.labels.map(label => label.id).filter(id => !listLabelIds.includes(id));
- return {
- label_ids: labelIds,
- };
- },
- /**
- * Seed the given patch request.
- *
- * (This is overridden in EE)
- */
- seedPatchRequest(issue, req) {
- return req;
- },
+ return {
+ label_ids: labelIds,
+ };
+ },
+ /**
+ * Seed the given patch request.
+ *
+ * (This is overridden in EE)
+ */
+ seedPatchRequest(issue, req) {
+ return req;
},
- });
+ },
+});
</script>
<template>
<div
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js
index acf41e5689e..c14d69c5d18 100644
--- a/app/assets/javascripts/boards/filtered_search_boards.js
+++ b/app/assets/javascripts/boards/filtered_search_boards.js
@@ -32,7 +32,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
const tokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token');
// Remove all the tokens as they will be replaced by the search manager
- [].forEach.call(tokens, (el) => {
+ [].forEach.call(tokens, el => {
el.parentNode.removeChild(el);
});
@@ -50,7 +50,10 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
canEdit(tokenName, tokenValue) {
if (this.cantEdit.includes(tokenName)) return false;
- return this.cantEditWithValue.findIndex(token => token.name === tokenName &&
- token.value === tokenValue) === -1;
+ return (
+ this.cantEditWithValue.findIndex(
+ token => token.name === tokenName && token.value === tokenValue,
+ ) === -1
+ );
}
}
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 91861f2f9ee..61a3072ac27 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -32,9 +32,9 @@ export default () => {
const $boardApp = document.getElementById('board-app');
// check for browser back and trigger a hard reload to circumvent browser caching.
- window.addEventListener('pageshow', (event) => {
- const isNavTypeBackForward = window.performance &&
- window.performance.navigation.type === NavigationType.TYPE_BACK_FORWARD;
+ window.addEventListener('pageshow', event => {
+ const isNavTypeBackForward =
+ window.performance && window.performance.navigation.type === NavigationType.TYPE_BACK_FORWARD;
if (event.persisted || isNavTypeBackForward) {
window.location.reload();
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js b/app/assets/javascripts/boards/mixins/sortable_default_options.js
index c9cde4effb9..983b28d2e67 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js
@@ -4,7 +4,8 @@ import $ from 'jquery';
import sortableConfig from '../../sortable/sortable_config';
export function sortableStart() {
- $('.has-tooltip').tooltip('hide')
+ $('.has-tooltip')
+ .tooltip('hide')
.tooltip('disable');
document.body.classList.add('is-dragging');
}
@@ -15,7 +16,8 @@ export function sortableEnd() {
}
export function getBoardSortableDefaultOptions(obj) {
- const touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch;
+ const touchEnabled =
+ 'ontouchstart' in window || (window.DocumentTouch && document instanceof DocumentTouch);
const defaultSortOptions = Object.assign({}, sortableConfig, {
filter: '.board-delete, .btn',
@@ -26,6 +28,8 @@ export function getBoardSortableDefaultOptions(obj) {
onEnd: sortableEnd,
});
- Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; });
+ Object.keys(obj).forEach(key => {
+ defaultSortOptions[key] = obj[key];
+ });
return defaultSortOptions;
}
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index 52d04389b88..5e0f0b07247 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -9,7 +9,7 @@ import IssueProject from './project';
import boardsStore from '../stores/boards_store';
class ListIssue {
- constructor (obj, defaultAvatar) {
+ constructor(obj, defaultAvatar) {
this.id = obj.id;
this.iid = obj.iid;
this.title = obj.title;
@@ -30,6 +30,8 @@ class ListIssue {
this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
this.milestone_id = obj.milestone_id;
this.project_id = obj.project_id;
+ this.timeEstimate = obj.time_estimate;
+ this.assignableLabelsEndpoint = obj.assignable_labels_endpoint;
if (obj.project) {
this.project = new IssueProject(obj.project);
@@ -39,54 +41,54 @@ class ListIssue {
this.milestone = new ListMilestone(obj.milestone);
}
- obj.labels.forEach((label) => {
+ obj.labels.forEach(label => {
this.labels.push(new ListLabel(label));
});
this.assignees = obj.assignees.map(a => new ListAssignee(a, defaultAvatar));
}
- addLabel (label) {
+ addLabel(label) {
if (!this.findLabel(label)) {
this.labels.push(new ListLabel(label));
}
}
- findLabel (findLabel) {
+ findLabel(findLabel) {
return this.labels.filter(label => label.title === findLabel.title)[0];
}
- removeLabel (removeLabel) {
+ removeLabel(removeLabel) {
if (removeLabel) {
this.labels = this.labels.filter(label => removeLabel.title !== label.title);
}
}
- removeLabels (labels) {
+ removeLabels(labels) {
labels.forEach(this.removeLabel.bind(this));
}
- addAssignee (assignee) {
+ addAssignee(assignee) {
if (!this.findAssignee(assignee)) {
this.assignees.push(new ListAssignee(assignee));
}
}
- findAssignee (findAssignee) {
+ findAssignee(findAssignee) {
return this.assignees.filter(assignee => assignee.id === findAssignee.id)[0];
}
- removeAssignee (removeAssignee) {
+ removeAssignee(removeAssignee) {
if (removeAssignee) {
this.assignees = this.assignees.filter(assignee => assignee.id !== removeAssignee.id);
}
}
- removeAllAssignees () {
+ removeAllAssignees() {
this.assignees = [];
}
- getLists () {
+ getLists() {
return boardsStore.state.lists.filter(list => list.findIssue(this.id));
}
@@ -102,14 +104,14 @@ class ListIssue {
this.isLoading[key] = value;
}
- update () {
+ update() {
const data = {
issue: {
milestone_id: this.milestone ? this.milestone.id : null,
due_date: this.dueDate,
- assignee_ids: this.assignees.length > 0 ? this.assignees.map((u) => u.id) : [0],
- label_ids: this.labels.map((label) => label.id)
- }
+ assignee_ids: this.assignees.length > 0 ? this.assignees.map(u => u.id) : [0],
+ label_ids: this.labels.map(label => label.id),
+ },
};
if (!data.issue.label_ids.length) {
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index 3161f1da8c9..dd3feedbc0e 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -234,11 +234,11 @@ class List {
});
}
- getTypeInfo (type) {
+ getTypeInfo(type) {
return TYPES[type] || {};
}
- onNewIssueResponse (issue, data) {
+ onNewIssueResponse(issue, data) {
issue.id = data.id;
issue.iid = data.iid;
issue.project = data.project;
diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js
index 029b0971f2c..3de6eb056c2 100644
--- a/app/assets/javascripts/boards/services/board_service.js
+++ b/app/assets/javascripts/boards/services/board_service.js
@@ -19,7 +19,9 @@ export default class BoardService {
}
static generateIssuePath(boardId, id) {
- return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${id ? `/${id}` : ''}`;
+ return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${
+ id ? `/${id}` : ''
+ }`;
}
all() {
@@ -54,7 +56,9 @@ export default class BoardService {
getIssuesForList(id, filter = {}) {
const data = { id };
- Object.keys(filter).forEach((key) => { data[key] = filter[key]; });
+ Object.keys(filter).forEach(key => {
+ data[key] = filter[key];
+ });
return axios.get(mergeUrlParams(data, this.generateIssuesPath(id)));
}
@@ -75,7 +79,9 @@ export default class BoardService {
}
getBacklog(data) {
- return axios.get(mergeUrlParams(data, `${gon.relative_url_root}/-/boards/${this.boardId}/issues.json`));
+ return axios.get(
+ mergeUrlParams(data, `${gon.relative_url_root}/-/boards/${this.boardId}/issues.json`),
+ );
}
bulkUpdate(issueIds, extraData = {}) {
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 471955747fd..eefe14a1d79 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -20,20 +20,20 @@ const boardsStore = {
issue: {},
list: {},
},
- create () {
+ create() {
this.state.lists = [];
this.filter.path = getUrlParamsArray().join('&');
this.detail = {
issue: {},
};
},
- addList (listObj, defaultAvatar) {
+ addList(listObj, defaultAvatar) {
const list = new List(listObj, defaultAvatar);
this.state.lists.push(list);
return list;
},
- new (listObj) {
+ new(listObj) {
const list = this.addList(listObj);
const backlogList = this.findList('type', 'backlog', 'backlog');
@@ -50,44 +50,44 @@ const boardsStore = {
});
this.removeBlankState();
},
- updateNewListDropdown (listId) {
+ updateNewListDropdown(listId) {
$(`.js-board-list-${listId}`).removeClass('is-active');
},
- shouldAddBlankState () {
+ shouldAddBlankState() {
// Decide whether to add the blank state
- return !(this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'closed')[0]);
+ return !this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'closed')[0];
},
- addBlankState () {
+ addBlankState() {
if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
this.addList({
id: 'blank',
list_type: 'blank',
title: 'Welcome to your Issue Board!',
- position: 0
+ position: 0,
});
this.state.lists = _.sortBy(this.state.lists, 'position');
},
- removeBlankState () {
+ removeBlankState() {
this.removeList('blank');
Cookies.set('issue_board_welcome_hidden', 'true', {
expires: 365 * 10,
- path: ''
+ path: '',
});
},
- welcomeIsHidden () {
+ welcomeIsHidden() {
return Cookies.get('issue_board_welcome_hidden') === 'true';
},
- removeList (id, type = 'blank') {
+ removeList(id, type = 'blank') {
const list = this.findList('id', id, type);
if (!list) return;
this.state.lists = this.state.lists.filter(list => list.id !== id);
},
- moveList (listFrom, orderLists) {
+ moveList(listFrom, orderLists) {
orderLists.forEach((id, i) => {
const list = this.findList('id', parseInt(id, 10));
@@ -95,22 +95,25 @@ const boardsStore = {
});
listFrom.update();
},
- moveIssueToList (listFrom, listTo, issue, newIndex) {
+ moveIssueToList(listFrom, listTo, issue, newIndex) {
const issueTo = listTo.findIssue(issue.id);
const issueLists = issue.getLists();
const listLabels = issueLists.map(listIssue => listIssue.label);
if (!issueTo) {
// Check if target list assignee is already present in this issue
- if ((listTo.type === 'assignee' && listFrom.type === 'assignee') &&
- issue.findAssignee(listTo.assignee)) {
+ if (
+ listTo.type === 'assignee' &&
+ listFrom.type === 'assignee' &&
+ issue.findAssignee(listTo.assignee)
+ ) {
const targetIssue = listTo.findIssue(issue.id);
targetIssue.removeAssignee(listFrom.assignee);
} else if (listTo.type === 'milestone') {
const currentMilestone = issue.milestone;
const currentLists = this.state.lists
- .filter(list => (list.type === 'milestone' && list.id !== listTo.id))
- .filter(list => list.issues.some(listIssue => issue.id === listIssue.id));
+ .filter(list => list.type === 'milestone' && list.id !== listTo.id)
+ .filter(list => list.issues.some(listIssue => issue.id === listIssue.id));
issue.removeMilestone(currentMilestone);
issue.addMilestone(listTo.milestone);
@@ -126,7 +129,7 @@ const boardsStore = {
}
if (listTo.type === 'closed' && listFrom.type !== 'backlog') {
- issueLists.forEach((list) => {
+ issueLists.forEach(list => {
list.removeIssue(issue);
});
issue.removeLabels(listLabels);
@@ -144,26 +147,28 @@ const boardsStore = {
return (
(listTo.type !== 'label' && listFrom.type === 'assignee') ||
(listTo.type !== 'assignee' && listFrom.type === 'label') ||
- (listFrom.type === 'backlog')
+ listFrom.type === 'backlog'
);
},
- moveIssueInList (list, issue, oldIndex, newIndex, idArray) {
+ moveIssueInList(list, issue, oldIndex, newIndex, idArray) {
const beforeId = parseInt(idArray[newIndex - 1], 10) || null;
const afterId = parseInt(idArray[newIndex + 1], 10) || null;
list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId);
},
- findList (key, val, type = 'label') {
- const filteredList = this.state.lists.filter((list) => {
- const byType = type ? (list.type === type) || (list.type === 'assignee') || (list.type === 'milestone') : true;
+ findList(key, val, type = 'label') {
+ const filteredList = this.state.lists.filter(list => {
+ const byType = type
+ ? list.type === type || list.type === 'assignee' || list.type === 'milestone'
+ : true;
return list[key] === val && byType;
});
return filteredList[0];
},
- updateFiltersUrl () {
+ updateFiltersUrl() {
window.history.pushState(null, null, `?${this.filter.path}`);
- }
+ },
};
// hacks added in order to allow milestone_select to function properly
diff --git a/app/assets/javascripts/boards/stores/modal_store.js b/app/assets/javascripts/boards/stores/modal_store.js
index 0d9ac367a70..b7228bf7bf5 100644
--- a/app/assets/javascripts/boards/stores/modal_store.js
+++ b/app/assets/javascripts/boards/stores/modal_store.js
@@ -40,7 +40,7 @@ class ModalStore {
toggleAll() {
const select = this.selectedCount() !== this.store.issues.length;
- this.store.issues.forEach((issue) => {
+ this.store.issues.forEach(issue => {
const issueUpdate = issue;
if (issueUpdate.selected !== select) {
@@ -69,13 +69,14 @@ class ModalStore {
removeSelectedIssue(issue, forcePurge = false) {
if (this.store.activeTab === 'all' || forcePurge) {
- this.store.selectedIssues = this.store.selectedIssues
- .filter(fIssue => fIssue.id !== issue.id);
+ this.store.selectedIssues = this.store.selectedIssues.filter(
+ fIssue => fIssue.id !== issue.id,
+ );
}
}
purgeUnselectedIssues() {
- this.store.selectedIssues.forEach((issue) => {
+ this.store.selectedIssues.forEach(issue => {
if (!issue.selected) {
this.removeSelectedIssue(issue, true);
}
@@ -87,8 +88,7 @@ class ModalStore {
}
findSelectedIssue(issue) {
- return this.store.selectedIssues
- .filter(filteredIssue => filteredIssue.id === issue.id)[0];
+ return this.store.selectedIssues.filter(filteredIssue => filteredIssue.id === issue.id)[0];
}
}
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index ebf76af5966..71fc2ac7d80 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -9,7 +9,7 @@ import eventHub from './event_hub';
import { APPLICATION_STATUS, REQUEST_LOADING, REQUEST_SUCCESS, REQUEST_FAILURE } from './constants';
import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store';
-import applications from './components/applications.vue';
+import Applications from './components/applications.vue';
import setupToggleButtons from '../toggle_buttons';
/**
@@ -28,8 +28,10 @@ export default class Clusters {
installIngressPath,
installRunnerPath,
installJupyterPath,
+ installKnativePath,
installPrometheusPath,
managePrometheusPath,
+ clusterType,
clusterStatus,
clusterStatusReason,
helpPath,
@@ -49,6 +51,7 @@ export default class Clusters {
installRunnerEndpoint: installRunnerPath,
installPrometheusEndpoint: installPrometheusPath,
installJupyterEndpoint: installJupyterPath,
+ installKnativeEndpoint: installKnativePath,
});
this.installApplication = this.installApplication.bind(this);
@@ -65,7 +68,7 @@ export default class Clusters {
initDismissableCallout('.js-cluster-security-warning');
initSettingsPanels();
setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area'));
- this.initApplications();
+ this.initApplications(clusterType);
if (this.store.state.status !== 'created') {
this.updateContainer(null, this.store.state.status, this.store.state.statusReason);
@@ -77,23 +80,21 @@ export default class Clusters {
}
}
- initApplications() {
+ initApplications(type) {
const { store } = this;
const el = document.querySelector('#js-cluster-applications');
this.applications = new Vue({
el,
- components: {
- applications,
- },
data() {
return {
state: store.state,
};
},
render(createElement) {
- return createElement('applications', {
+ return createElement(Applications, {
props: {
+ type,
applications: this.state.applications,
helpPath: this.state.helpPath,
ingressHelpPath: this.state.ingressHelpPath,
diff --git a/app/assets/javascripts/clusters/clusters_index.js b/app/assets/javascripts/clusters/clusters_index.js
deleted file mode 100644
index 789c8360124..00000000000
--- a/app/assets/javascripts/clusters/clusters_index.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import createFlash from '~/flash';
-import { __ } from '~/locale';
-import setupToggleButtons from '~/toggle_buttons';
-import initDismissableCallout from '~/dismissable_callout';
-
-import ClustersService from './services/clusters_service';
-
-export default () => {
- const clusterList = document.querySelector('.js-clusters-list');
-
- initDismissableCallout('.gcp-signup-offer');
-
- // The empty state won't have a clusterList
- if (clusterList) {
- setupToggleButtons(document.querySelector('.js-clusters-list'), (value, toggle) =>
- ClustersService.updateCluster(toggle.dataset.endpoint, { cluster: { enabled: value } }).catch(
- err => {
- createFlash(__('Something went wrong on our end.'));
- throw err;
- },
- ),
- );
- }
-};
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index 6e7b5eb5526..c1026d1273a 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -1,18 +1,19 @@
<script>
import _ from 'underscore';
-import helmInstallIllustration from '@gitlab-org/gitlab-svgs/dist/illustrations/kubernetes-installation.svg';
+import helmInstallIllustration from '@gitlab/svgs/dist/illustrations/kubernetes-installation.svg';
import elasticsearchLogo from 'images/cluster_app_logos/elasticsearch.png';
import gitlabLogo from 'images/cluster_app_logos/gitlab.png';
import helmLogo from 'images/cluster_app_logos/helm.png';
import jeagerLogo from 'images/cluster_app_logos/jeager.png';
import jupyterhubLogo from 'images/cluster_app_logos/jupyterhub.png';
import kubernetesLogo from 'images/cluster_app_logos/kubernetes.png';
+import knativeLogo from 'images/cluster_app_logos/knative.png';
import meltanoLogo from 'images/cluster_app_logos/meltano.png';
import prometheusLogo from 'images/cluster_app_logos/prometheus.png';
import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
-import { APPLICATION_STATUS, INGRESS } from '../constants';
+import { CLUSTER_TYPE, APPLICATION_STATUS, INGRESS } from '../constants';
export default {
components: {
@@ -20,6 +21,11 @@ export default {
clipboardButton,
},
props: {
+ type: {
+ type: String,
+ required: false,
+ default: CLUSTER_TYPE.PROJECT,
+ },
applications: {
type: Object,
required: false,
@@ -53,10 +59,14 @@ export default {
jeagerLogo,
jupyterhubLogo,
kubernetesLogo,
+ knativeLogo,
meltanoLogo,
prometheusLogo,
}),
computed: {
+ isProjectCluster() {
+ return this.type === CLUSTER_TYPE.PROJECT;
+ },
helmInstalled() {
return (
this.applications.helm.status === APPLICATION_STATUS.INSTALLED ||
@@ -136,6 +146,9 @@ export default {
jupyterHostname() {
return this.applications.jupyter.hostname;
},
+ knativeInstalled() {
+ return this.applications.knative.status === APPLICATION_STATUS.INSTALLED;
+ },
},
created() {
this.helmInstallIllustration = helmInstallIllustration;
@@ -276,6 +289,7 @@ export default {
</div>
</application-row>
<application-row
+ v-if="isProjectCluster"
id="prometheus"
:logo-url="prometheusLogo"
:title="applications.prometheus.title"
@@ -294,6 +308,7 @@ export default {
</div>
</application-row>
<application-row
+ v-if="isProjectCluster"
id="runner"
:logo-url="gitlabLogo"
:title="applications.runner.title"
@@ -312,6 +327,7 @@ export default {
</div>
</application-row>
<application-row
+ v-if="isProjectCluster"
id="jupyter"
:logo-url="jupyterhubLogo"
:title="applications.jupyter.title"
@@ -321,7 +337,6 @@ export default {
:request-reason="applications.jupyter.requestReason"
:install-application-request-params="{ hostname: applications.jupyter.hostname }"
:disabled="!helmInstalled"
- class="hide-bottom-border rounded-bottom"
title-link="https://jupyterhub.readthedocs.io/en/stable/"
>
<div slot="description">
@@ -371,6 +386,58 @@ export default {
</template>
</div>
</application-row>
+ <application-row
+ id="knative"
+ :logo-url="knativeLogo"
+ :title="applications.knative.title"
+ :status="applications.knative.status"
+ :status-reason="applications.knative.statusReason"
+ :request-status="applications.knative.requestStatus"
+ :request-reason="applications.knative.requestReason"
+ :install-application-request-params="{ hostname: applications.knative.hostname}"
+ :disabled="!helmInstalled"
+ class="hide-bottom-border rounded-bottom"
+ title-link="https://github.com/knative/docs"
+ >
+ <div slot="description">
+ <p>
+ {{ s__(`ClusterIntegration|A Knative build extends Kubernetes
+ and utilizes existing Kubernetes primitives to provide you with
+ the ability to run on-cluster container builds from source.
+ For example, you can write a build that uses Kubernetes-native
+ resources to obtain your source code from a repository,
+ build it into container a image, and then run that image.`) }}
+ </p>
+
+ <template v-if="knativeInstalled">
+ <div class="form-group">
+ <label for="knative-domainname">
+ {{ s__('ClusterIntegration|Knative Domain Name:') }}
+ </label>
+ <input
+ id="knative-domainname"
+ v-model="applications.knative.hostname"
+ type="text"
+ class="form-control js-domainname"
+ readonly
+ />
+ </div>
+ </template>
+ <template v-else>
+ <div class="form-group">
+ <label for="knative-domainname">
+ {{ s__('ClusterIntegration|Knative Domain Name:') }}
+ </label>
+ <input
+ id="knative-domainname"
+ v-model="applications.knative.hostname"
+ type="text"
+ class="form-control js-domainname"
+ />
+ </div>
+ </template>
+ </div>
+ </application-row>
</div>
</section>
</template>
diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js
index 24a49624583..15cf4a56138 100644
--- a/app/assets/javascripts/clusters/constants.js
+++ b/app/assets/javascripts/clusters/constants.js
@@ -1,3 +1,10 @@
+// These need to match the enum found in app/models/clusters/cluster.rb
+export const CLUSTER_TYPE = {
+ INSTANCE: 'instance_type',
+ GROUP: 'group_type',
+ PROJECT: 'project_type',
+};
+
// These need to match what is returned from the server
export const APPLICATION_STATUS = {
NOT_INSTALLABLE: 'not_installable',
@@ -16,3 +23,4 @@ export const REQUEST_SUCCESS = 'request-success';
export const REQUEST_FAILURE = 'request-failure';
export const INGRESS = 'ingress';
export const JUPYTER = 'jupyter';
+export const KNATIVE = 'knative';
diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js
index a7d82292ba9..da562b09ee5 100644
--- a/app/assets/javascripts/clusters/services/clusters_service.js
+++ b/app/assets/javascripts/clusters/services/clusters_service.js
@@ -9,6 +9,7 @@ export default class ClusterService {
runner: this.options.installRunnerEndpoint,
prometheus: this.options.installPrometheusEndpoint,
jupyter: this.options.installJupyterEndpoint,
+ knative: this.options.installKnativeEndpoint,
};
}
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index 106ac3cb516..e45da967392 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -1,5 +1,5 @@
import { s__ } from '../../locale';
-import { INGRESS, JUPYTER } from '../constants';
+import { INGRESS, JUPYTER, KNATIVE } from '../constants';
export default class ClusterStore {
constructor() {
@@ -46,6 +46,14 @@ export default class ClusterStore {
requestReason: null,
hostname: null,
},
+ knative: {
+ title: s__('ClusterIntegration|Knative'),
+ status: null,
+ statusReason: null,
+ requestStatus: null,
+ requestReason: null,
+ hostname: null,
+ },
},
};
}
@@ -93,6 +101,9 @@ export default class ClusterStore {
(this.state.applications.ingress.externalIp
? `jupyter.${this.state.applications.ingress.externalIp}.nip.io`
: '');
+ } else if (appId === KNATIVE) {
+ this.state.applications.knative.hostname =
+ serverAppEntry.hostname || this.state.applications.knative.hostname;
}
});
}
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index a2aa3d197e3..82532539c9c 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -2,9 +2,15 @@
import PipelinesService from '../../pipelines/services/pipelines_service';
import PipelineStore from '../../pipelines/stores/pipelines_store';
import pipelinesMixin from '../../pipelines/mixins/pipelines';
+import TablePagination from '../../vue_shared/components/table_pagination.vue';
+import { getParameterByName } from '../../lib/utils/common_utils';
+import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
export default {
- mixins: [pipelinesMixin],
+ components: {
+ TablePagination,
+ },
+ mixins: [pipelinesMixin, CIPaginationMixin],
props: {
endpoint: {
type: String,
@@ -35,6 +41,8 @@ export default {
return {
store,
state: store.state,
+ page: getParameterByName('page') || '1',
+ requestData: {},
};
},
@@ -48,11 +56,14 @@ export default {
},
created() {
this.service = new PipelinesService(this.endpoint);
+ this.requestData = { page: this.page };
},
methods: {
successCallback(resp) {
// depending of the endpoint the response can either bring a `pipelines` key or not.
const pipelines = resp.data.pipelines || resp.data;
+
+ this.store.storePagination(resp.headers);
this.setCommonData(pipelines);
const updatePipelinesEvent = new CustomEvent('update-pipelines-count', {
@@ -97,5 +108,11 @@ export default {
:view-type="viewType"
/>
</div>
+
+ <table-pagination
+ v-if="shouldRenderPagination"
+ :change="onChangePage"
+ :page-info="state.pageInfo"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/commons/gitlab_ui.js b/app/assets/javascripts/commons/gitlab_ui.js
deleted file mode 100644
index f60665577fe..00000000000
--- a/app/assets/javascripts/commons/gitlab_ui.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import Vue from 'vue';
-import {
- GlPagination,
- GlProgressBar,
- GlModal,
- GlLoadingIcon,
- GlModalDirective,
- GlTooltipDirective,
-} from '@gitlab-org/gitlab-ui';
-
-Vue.component('gl-pagination', GlPagination);
-Vue.component('gl-progress-bar', GlProgressBar);
-Vue.component('gl-ui-modal', GlModal);
-Vue.component('gl-loading-icon', GlLoadingIcon);
-
-Vue.directive('gl-modal', GlModalDirective);
-Vue.directive('gl-tooltip', GlTooltipDirective);
diff --git a/app/assets/javascripts/commons/index.js b/app/assets/javascripts/commons/index.js
index ea945cd3fa5..0d2fe2925d8 100644
--- a/app/assets/javascripts/commons/index.js
+++ b/app/assets/javascripts/commons/index.js
@@ -3,5 +3,4 @@ import './polyfills';
import './jquery';
import './bootstrap';
import './vue';
-import './gitlab_ui';
import '../lib/utils/axios_utils';
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
index 539d0d29e0d..bffc025ced3 100644
--- a/app/assets/javascripts/commons/polyfills.js
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -5,6 +5,7 @@ import 'core-js/fn/array/find-index';
import 'core-js/fn/array/from';
import 'core-js/fn/array/includes';
import 'core-js/fn/object/assign';
+import 'core-js/fn/object/values';
import 'core-js/fn/promise';
import 'core-js/fn/string/code-point-at';
import 'core-js/fn/string/from-code-point';
diff --git a/app/assets/javascripts/deploy_keys/components/action_btn.vue b/app/assets/javascripts/deploy_keys/components/action_btn.vue
index 10548da8ec5..ea74fd27ff6 100644
--- a/app/assets/javascripts/deploy_keys/components/action_btn.vue
+++ b/app/assets/javascripts/deploy_keys/components/action_btn.vue
@@ -1,7 +1,11 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import eventHub from '../eventhub';
export default {
+ components: {
+ GlLoadingIcon,
+ },
props: {
deployKey: {
type: Object,
diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue
index 3589599986d..631a9673b3e 100644
--- a/app/assets/javascripts/deploy_keys/components/app.vue
+++ b/app/assets/javascripts/deploy_keys/components/app.vue
@@ -6,11 +6,13 @@ import eventHub from '../eventhub';
import DeployKeysService from '../service';
import DeployKeysStore from '../store';
import KeysPanel from './keys_panel.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
components: {
KeysPanel,
NavigationTabs,
+ GlLoadingIcon,
},
props: {
endpoint: {
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 59680959bb1..b885fa49365 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -3,6 +3,7 @@ import { mapState, mapGetters, mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import { __ } from '~/locale';
import createFlash from '~/flash';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import eventHub from '../../notes/event_hub';
import CompareVersions from './compare_versions.vue';
import DiffFile from './diff_file.vue';
@@ -21,6 +22,7 @@ export default {
HiddenFilesWarning,
CommitWidget,
TreeList,
+ GlLoadingIcon,
},
props: {
endpoint: {
@@ -92,7 +94,7 @@ export default {
return __('Show latest version');
},
canCurrentUserFork() {
- return this.currentUser.canFork === true && this.currentUser.canCreateMergeRequest;
+ return this.currentUser.can_fork === true && this.currentUser.can_create_merge_request;
},
showCompareVersions() {
return this.mergeRequestDiffs && this.mergeRequestDiff;
@@ -126,6 +128,7 @@ export default {
eventHub.$once('fetchedNotesData', this.setDiscussions);
},
methods: {
+ ...mapActions(['startTaskList']),
...mapActions('diffs', [
'setBaseConfig',
'fetchDiffFiles',
@@ -155,7 +158,13 @@ export default {
if (this.isNotesFetched && !this.assignedDiscussions && !this.isLoading) {
this.assignedDiscussions = true;
- requestIdleCallback(() => this.assignDiscussionsToDiff(), { timeout: 1000 });
+ requestIdleCallback(
+ () =>
+ this.assignDiscussionsToDiff()
+ .then(this.$nextTick)
+ .then(this.startTaskList),
+ { timeout: 1000 },
+ );
}
},
adjustView() {
@@ -223,7 +232,10 @@ export default {
:commit="commit"
/>
- <div class="files d-flex prepend-top-default">
+ <div
+ :data-can-create-note="getNoteableData.current_user.can_create_note"
+ class="files d-flex prepend-top-default"
+ >
<div
v-show="showTreeList"
class="diff-tree-list"
diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue
index 23d0bad2ecb..aa72aca1478 100644
--- a/app/assets/javascripts/diffs/components/commit_item.vue
+++ b/app/assets/javascripts/diffs/components/commit_item.vue
@@ -40,15 +40,17 @@ export default {
},
computed: {
authorName() {
- return (this.commit.author && this.commit.author.name) || this.commit.authorName;
+ return (this.commit.author && this.commit.author.name) || this.commit.author_name;
},
authorUrl() {
return (
- (this.commit.author && this.commit.author.webUrl) || `mailto:${this.commit.authorEmail}`
+ (this.commit.author && this.commit.author.web_url) || `mailto:${this.commit.author_email}`
);
},
authorAvatar() {
- return (this.commit.author && this.commit.author.avatarUrl) || this.commit.authorGravatarUrl;
+ return (
+ (this.commit.author && this.commit.author.avatar_url) || this.commit.author_gravatar_url
+ );
},
},
};
@@ -66,18 +68,18 @@ export default {
<div class="commit-detail flex-list">
<div class="commit-content qa-commit-content">
<a
- :href="commit.commitUrl"
+ :href="commit.commit_url"
class="commit-row-message item-title"
- v-html="commit.titleHtml"
+ v-html="commit.title_html"
></a>
<span class="commit-row-message d-block d-sm-none">
&middot;
- {{ commit.shortId }}
+ {{ commit.short_id }}
</span>
<button
- v-if="commit.descriptionHtml"
+ v-if="commit.description_html"
class="text-expander js-toggle-button"
type="button"
:aria-label="__('Toggle commit description')"
@@ -95,29 +97,29 @@ export default {
></a>
{{ s__('CommitWidget|authored') }}
<time-ago-tooltip
- :time="commit.authoredDate"
+ :time="commit.authored_date"
/>
</div>
<pre
- v-if="commit.descriptionHtml"
+ v-if="commit.description_html"
class="commit-row-description js-toggle-content append-bottom-8"
- v-html="commit.descriptionHtml"
+ v-html="commit.description_html"
></pre>
</div>
<div class="commit-actions flex-row d-none d-sm-flex">
<div
- v-if="commit.signatureHtml"
- v-html="commit.signatureHtml"
+ v-if="commit.signature_html"
+ v-html="commit.signature_html"
></div>
<commit-pipeline-status
- v-if="commit.pipelineStatusPath"
- :endpoint="commit.pipelineStatusPath"
+ v-if="commit.pipeline_status_path"
+ :endpoint="commit.pipeline_status_path"
/>
<div class="commit-sha-group">
<div
class="label label-monospace"
- v-text="commit.shortId"
+ v-text="commit.short_id"
></div>
<clipboard-button
:text="commit.id"
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
index 29b5aff0fb1..a5b87dfc2d9 100644
--- a/app/assets/javascripts/diffs/components/compare_versions.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -36,7 +36,7 @@ export default {
},
computed: {
...mapState('diffs', ['commit', 'showTreeList']),
- ...mapGetters('diffs', ['isInlineView', 'isParallelView', 'areAllFilesCollapsed']),
+ ...mapGetters('diffs', ['isInlineView', 'isParallelView', 'hasCollapsedFile']),
comparableDiffs() {
return this.mergeRequestDiffs.slice(1);
},
@@ -113,8 +113,8 @@ export default {
class="inline-parallel-buttons d-none d-md-flex ml-auto"
>
<a
- v-if="areAllFilesCollapsed"
- class="btn btn-default"
+ v-show="hasCollapsedFile"
+ class="btn btn-default append-right-8"
@click="expandAllFiles"
>
{{ __('Expand all') }}
diff --git a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
index f4b333f3700..112206e4ad6 100644
--- a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
@@ -56,16 +56,16 @@ export default {
methods: {
commitsText(version) {
return n__(
- `${version.commitsCount} commit,`,
- `${version.commitsCount} commits,`,
- version.commitsCount,
+ `${version.commits_count} commit,`,
+ `${version.commits_count} commits,`,
+ version.commits_count,
);
},
href(version) {
if (this.showCommitCount) {
- return version.versionPath;
+ return version.version_path;
}
- return version.comparePath;
+ return version.compare_path;
},
versionName(version) {
if (this.isLatest(version)) {
@@ -74,7 +74,7 @@ export default {
if (this.targetBranch && (this.isBase(version) || !version)) {
return this.targetBranch.branchName;
}
- return `version ${version.versionIndex}`;
+ return `version ${version.version_index}`;
},
isActive(version) {
if (!version) {
@@ -84,11 +84,11 @@ export default {
if (this.targetBranch) {
return (
(this.isBase(version) && !this.startVersion) ||
- (this.startVersion && this.startVersion.versionIndex === version.versionIndex)
+ (this.startVersion && this.startVersion.version_index === version.version_index)
);
}
- return version.versionIndex === this.mergeRequestVersion.versionIndex;
+ return version.version_index === this.mergeRequestVersion.version_index;
},
isBase(version) {
if (!version || !this.targetBranch) {
@@ -98,7 +98,7 @@ export default {
},
isLatest(version) {
return (
- this.mergeRequestVersion && version.versionIndex === this.targetVersions[0].versionIndex
+ this.mergeRequestVersion && version.version_index === this.targetVersions[0].version_index
);
},
},
@@ -142,7 +142,7 @@ export default {
</div>
<div>
<small class="commit-sha">
- {{ version.truncatedCommitSha }}
+ {{ version.truncated_commit_sha }}
</small>
</div>
<div>
@@ -151,8 +151,8 @@ export default {
{{ commitsText(version) }}
</template>
<time-ago
- v-if="version.createdAt"
- :time="version.createdAt"
+ v-if="version.created_at"
+ :time="version.created_at"
class="js-timeago js-timeago-render"
/>
</small>
diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue
index fb5556e3cd7..5e5fda5fba6 100644
--- a/app/assets/javascripts/diffs/components/diff_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_content.vue
@@ -1,15 +1,22 @@
<script>
-import { mapGetters, mapState } from 'vuex';
+import { mapActions, mapGetters, mapState } from 'vuex';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
-import { diffModes } from '~/ide/constants';
import InlineDiffView from './inline_diff_view.vue';
import ParallelDiffView from './parallel_diff_view.vue';
+import NoteForm from '../../notes/components/note_form.vue';
+import ImageDiffOverlay from './image_diff_overlay.vue';
+import DiffDiscussions from './diff_discussions.vue';
+import { IMAGE_DIFF_POSITION_TYPE } from '../constants';
+import { getDiffMode } from '../store/utils';
export default {
components: {
InlineDiffView,
ParallelDiffView,
DiffViewer,
+ NoteForm,
+ DiffDiscussions,
+ ImageDiffOverlay,
},
props: {
diffFile: {
@@ -23,13 +30,38 @@ export default {
endpoint: state => state.diffs.endpoint,
}),
...mapGetters('diffs', ['isInlineView', 'isParallelView']),
+ ...mapGetters('diffs', ['getCommentFormForDiffFile']),
+ ...mapGetters(['getNoteableData', 'noteableType']),
diffMode() {
- const diffModeKey = Object.keys(diffModes).find(key => this.diffFile[`${key}File`]);
- return diffModes[diffModeKey] || diffModes.replaced;
+ return getDiffMode(this.diffFile);
},
isTextFile() {
return this.diffFile.viewer.name === 'text';
},
+ diffFileCommentForm() {
+ return this.getCommentFormForDiffFile(this.diffFile.file_hash);
+ },
+ showNotesContainer() {
+ return this.diffFile.discussions.length || this.diffFileCommentForm;
+ },
+ },
+ methods: {
+ ...mapActions('diffs', ['saveDiffDiscussion', 'closeDiffFileCommentForm']),
+ handleSaveNote(note) {
+ this.saveDiffDiscussion({
+ note,
+ formData: {
+ noteableData: this.getNoteableData,
+ noteableType: this.noteableType,
+ diffFile: this.diffFile,
+ positionType: IMAGE_DIFF_POSITION_TYPE,
+ x: this.diffFileCommentForm.x,
+ y: this.diffFileCommentForm.y,
+ width: this.diffFileCommentForm.width,
+ height: this.diffFileCommentForm.height,
+ },
+ });
+ },
},
};
</script>
@@ -41,22 +73,52 @@ export default {
<inline-diff-view
v-if="isInlineView"
:diff-file="diffFile"
- :diff-lines="diffFile.highlightedDiffLines || []"
+ :diff-lines="diffFile.highlighted_diff_lines || []"
/>
<parallel-diff-view
v-if="isParallelView"
:diff-file="diffFile"
- :diff-lines="diffFile.parallelDiffLines || []"
+ :diff-lines="diffFile.parallel_diff_lines || []"
/>
</template>
<diff-viewer
v-else
:diff-mode="diffMode"
- :new-path="diffFile.newPath"
- :new-sha="diffFile.diffRefs.headSha"
- :old-path="diffFile.oldPath"
- :old-sha="diffFile.diffRefs.baseSha"
- :project-path="projectPath"/>
+ :new-path="diffFile.new_path"
+ :new-sha="diffFile.diff_refs.head_sha"
+ :old-path="diffFile.old_path"
+ :old-sha="diffFile.diff_refs.base_sha"
+ :file-hash="diffFile.file_hash"
+ :project-path="projectPath"
+ >
+ <image-diff-overlay
+ slot="image-overlay"
+ :discussions="diffFile.discussions"
+ :file-hash="diffFile.file_hash"
+ :can-comment="getNoteableData.current_user.can_create_note"
+ />
+ <div
+ v-if="showNotesContainer"
+ class="note-container"
+ >
+ <diff-discussions
+ v-if="diffFile.discussions.length"
+ class="diff-file-discussions"
+ :discussions="diffFile.discussions"
+ :should-collapse-discussions="true"
+ :render-avatar-badge="true"
+ />
+ <note-form
+ v-if="diffFileCommentForm"
+ ref="noteForm"
+ :is-editing="false"
+ :save-button-title="__('Comment')"
+ class="diff-comment-form new-note discussion-form discussion-form-container"
+ @handleFormUpdate="handleSaveNote"
+ @cancelForm="closeDiffFileCommentForm(diffFile.file_hash)"
+ />
+ </div>
+ </diff-viewer>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/diffs/components/diff_discussions.vue b/app/assets/javascripts/diffs/components/diff_discussions.vue
index cddbe554fbd..b9de487a737 100644
--- a/app/assets/javascripts/diffs/components/diff_discussions.vue
+++ b/app/assets/javascripts/diffs/components/diff_discussions.vue
@@ -1,24 +1,40 @@
<script>
import { mapActions } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
import noteableDiscussion from '../../notes/components/noteable_discussion.vue';
export default {
components: {
noteableDiscussion,
+ Icon,
},
props: {
discussions: {
type: Array,
required: true,
},
+ shouldCollapseDiscussions: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ renderAvatarBadge: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
methods: {
+ ...mapActions(['toggleDiscussion']),
...mapActions('diffs', ['removeDiscussionsFromDiff']),
deleteNoteHandler(discussion) {
if (discussion.notes.length <= 1) {
this.removeDiscussionsFromDiff(discussion);
}
},
+ isExpanded(discussion) {
+ return this.shouldCollapseDiscussions ? discussion.expanded : true;
+ },
},
};
</script>
@@ -26,22 +42,53 @@ export default {
<template>
<div>
<div
- v-for="discussion in discussions"
+ v-for="(discussion, index) in discussions"
:key="discussion.id"
- class="discussion-notes diff-discussions"
+ :class="{
+ collapsed: !isExpanded(discussion)
+ }"
+ class="discussion-notes diff-discussions position-relative"
>
<ul
:data-discussion-id="discussion.id"
class="notes"
>
+ <template v-if="shouldCollapseDiscussions">
+ <button
+ :class="{
+ 'diff-notes-collapse': discussion.expanded,
+ 'btn-transparent badge badge-pill': !discussion.expanded
+ }"
+ type="button"
+ class="js-diff-notes-toggle"
+ @click="toggleDiscussion({ discussionId: discussion.id })"
+ >
+ <icon
+ v-if="discussion.expanded"
+ name="collapse"
+ class="collapse-icon"
+ />
+ <template v-else>
+ {{ index + 1 }}
+ </template>
+ </button>
+ </template>
<noteable-discussion
+ v-show="isExpanded(discussion)"
:discussion="discussion"
- :render-header="false"
:render-diff-file="false"
:always-expanded="true"
:discussions-by-diff-order="true"
@noteDeleted="deleteNoteHandler"
- />
+ >
+ <span
+ v-if="renderAvatarBadge"
+ slot="avatar-badge"
+ class="badge badge-pill"
+ >
+ {{ index + 1 }}
+ </span>
+ </noteable-discussion>
</ul>
</div>
</div>
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index 958e57c5652..872131a5900 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -3,6 +3,7 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore';
import { __, sprintf } from '~/locale';
import createFlash from '~/flash';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import DiffFileHeader from './diff_file_header.vue';
import DiffContent from './diff_content.vue';
@@ -10,6 +11,7 @@ export default {
components: {
DiffFileHeader,
DiffContent,
+ GlLoadingIcon,
},
props: {
file: {
@@ -30,6 +32,7 @@ export default {
computed: {
...mapState('diffs', ['currentDiffFileId']),
...mapGetters(['isNotesFetched']),
+ ...mapGetters('diffs', ['getDiffFileDiscussions']),
isCollapsed() {
return this.file.collapsed || false;
},
@@ -37,7 +40,7 @@ export default {
return sprintf(
__('You can %{linkStart}view the blob%{linkEnd} instead.'),
{
- linkStart: `<a href="${_.escape(this.file.viewPath)}">`,
+ linkStart: `<a href="${_.escape(this.file.view_path)}">`,
linkEnd: '</a>',
},
false,
@@ -46,21 +49,34 @@ export default {
showExpandMessage() {
return (
this.isCollapsed ||
- (!this.file.highlightedDiffLines &&
+ (!this.file.highlighted_diff_lines &&
!this.isLoadingCollapsedDiff &&
- !this.file.tooLarge &&
+ !this.file.too_large &&
this.file.text)
);
},
showLoadingIcon() {
return this.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed);
},
+ hasDiffLines() {
+ return (
+ this.file.highlighted_diff_lines &&
+ this.file.parallel_diff_lines &&
+ this.file.parallel_diff_lines.length > 0
+ );
+ },
+ },
+ watch: {
+ 'file.collapsed': function fileCollapsedWatch(newVal, oldVal) {
+ if (!newVal && oldVal && !this.hasDiffLines) {
+ this.handleLoadCollapsedDiff();
+ }
+ },
},
methods: {
...mapActions('diffs', ['loadCollapsedDiff', 'assignDiscussionsToDiff']),
handleToggle() {
- const { highlightedDiffLines, parallelDiffLines } = this.file;
- if (!highlightedDiffLines && parallelDiffLines !== undefined && !parallelDiffLines.length) {
+ if (!this.hasDiffLines) {
this.handleLoadCollapsedDiff();
} else {
this.file.collapsed = !this.file.collapsed;
@@ -79,7 +95,7 @@ export default {
.then(() => {
requestIdleCallback(
() => {
- this.assignDiscussionsToDiff();
+ this.assignDiscussionsToDiff(this.getDiffFileDiscussions(this.file));
},
{ timeout: 1000 },
);
@@ -101,9 +117,9 @@ export default {
<template>
<div
- :id="file.fileHash"
+ :id="file.file_hash"
:class="{
- 'is-active': currentDiffFileId === file.fileHash
+ 'is-active': currentDiffFileId === file.file_hash
}"
class="diff-file file-holder"
>
@@ -127,7 +143,7 @@ export default {
make your changes there, and submit a merge request.
</span>
<a
- :href="file.forkPath"
+ :href="file.fork_path"
class="js-fork-suggestion-button btn btn-grouped btn-inverted btn-success"
>
Fork
@@ -143,7 +159,7 @@ export default {
<diff-content
v-if="!isCollapsed && file.renderIt"
- :class="{ hidden: isCollapsed || file.tooLarge }"
+ :class="{ hidden: isCollapsed || file.too_large }"
:diff-file="file"
/>
<gl-loading-icon
@@ -164,7 +180,7 @@ export default {
</a>
</div>
<div
- v-if="file.tooLarge"
+ v-if="file.too_large"
class="nothing-here-block diff-collapsed js-too-large-diff"
>
{{ __('This source diff could not be displayed because it is too large.') }}
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index dcf1057eb84..af03cec6582 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -68,32 +68,32 @@ export default {
},
titleLink() {
if (this.diffFile.submodule) {
- return this.diffFile.submoduleTreeUrl || this.diffFile.submoduleLink;
+ return this.diffFile.submodule_tree_url || this.diffFile.submodule_link;
}
return this.discussionPath;
},
filePath() {
if (this.diffFile.submodule) {
- return `${this.diffFile.filePath} @ ${truncateSha(this.diffFile.blob.id)}`;
+ return `${this.diffFile.file_path} @ ${truncateSha(this.diffFile.blob.id)}`;
}
- if (this.diffFile.deletedFile) {
- return sprintf(__('%{filePath} deleted'), { filePath: this.diffFile.filePath }, false);
+ if (this.diffFile.deleted_file) {
+ return sprintf(__('%{filePath} deleted'), { filePath: this.diffFile.file_path }, false);
}
- return this.diffFile.filePath;
+ return this.diffFile.file_path;
},
titleTag() {
- return this.diffFile.fileHash ? 'a' : 'span';
+ return this.diffFile.file_hash ? 'a' : 'span';
},
isUsingLfs() {
- return this.diffFile.storedExternally && this.diffFile.externalStorage === 'lfs';
+ return this.diffFile.stored_externally && this.diffFile.external_storage === 'lfs';
},
collapseIcon() {
return this.expanded ? 'chevron-down' : 'chevron-right';
},
viewFileButtonText() {
- const truncatedContentSha = _.escape(truncateSha(this.diffFile.contentSha));
+ const truncatedContentSha = _.escape(truncateSha(this.diffFile.content_sha));
return sprintf(
s__('MergeRequests|View file @ %{commitId}'),
{
@@ -103,7 +103,7 @@ export default {
);
},
viewReplacedFileButtonText() {
- const truncatedBaseSha = _.escape(truncateSha(this.diffFile.diffRefs.baseSha));
+ const truncatedBaseSha = _.escape(truncateSha(this.diffFile.diff_refs.base_sha));
return sprintf(
s__('MergeRequests|View replaced file @ %{commitId}'),
{
@@ -113,7 +113,7 @@ export default {
);
},
gfmCopyText() {
- return `\`${this.diffFile.filePath}\``;
+ return `\`${this.diffFile.file_path}\``;
},
},
methods: {
@@ -164,21 +164,21 @@ export default {
aria-hidden="true"
css-classes="js-file-icon append-right-5"
/>
- <span v-if="diffFile.renamedFile">
+ <span v-if="diffFile.renamed_file">
<strong
v-tooltip
- :title="diffFile.oldPath"
+ :title="diffFile.old_path"
class="file-title-name"
data-container="body"
- v-html="diffFile.oldPathHtml"
+ v-html="diffFile.old_path_html"
></strong>
<strong
v-tooltip
- :title="diffFile.newPath"
+ :title="diffFile.new_path"
class="file-title-name"
data-container="body"
- v-html="diffFile.newPathHtml"
+ v-html="diffFile.new_path_html"
></strong>
</span>
@@ -195,16 +195,16 @@ export default {
<clipboard-button
:title="__('Copy file path to clipboard')"
- :text="diffFile.filePath"
+ :text="diffFile.file_path"
:gfm="gfmCopyText"
css-class="btn-default btn-transparent btn-clipboard"
/>
<small
- v-if="diffFile.modeChanged"
+ v-if="diffFile.mode_changed"
ref="fileMode"
>
- {{ diffFile.aMode }} → {{ diffFile.bMode }}
+ {{ diffFile.a_mode }} → {{ diffFile.b_mode }}
</small>
<span
@@ -220,7 +220,7 @@ export default {
class="file-actions d-none d-sm-block"
>
<template
- v-if="diffFile.blob && diffFile.blob.readableText"
+ v-if="diffFile.blob && diffFile.blob.readable_text"
>
<button
:disabled="!diffHasDiscussions(diffFile)"
@@ -234,33 +234,33 @@ export default {
</button>
<edit-button
- v-if="!diffFile.deletedFile"
+ v-if="!diffFile.deleted_file"
:can-current-user-fork="canCurrentUserFork"
- :edit-path="diffFile.editPath"
- :can-modify-blob="diffFile.canModifyBlob"
+ :edit-path="diffFile.edit_path"
+ :can-modify-blob="diffFile.can_modify_blob"
@showForkMessage="showForkMessage"
/>
</template>
<a
- v-if="diffFile.replacedViewPath"
- :href="diffFile.replacedViewPath"
+ v-if="diffFile.replaced_view_path"
+ :href="diffFile.replaced_view_path"
class="btn view-file js-view-file"
v-html="viewReplacedFileButtonText"
>
</a>
<a
- :href="diffFile.viewPath"
+ :href="diffFile.view_path"
class="btn view-file js-view-file"
v-html="viewFileButtonText"
>
</a>
<a
- v-if="diffFile.externalUrl"
+ v-if="diffFile.external_url"
v-tooltip
- :href="diffFile.externalUrl"
- :title="`View on ${diffFile.formattedExternalUrl}`"
+ :href="diffFile.external_url"
+ :title="`View on ${diffFile.formatted_external_url}`"
target="_blank"
rel="noopener noreferrer"
class="btn btn-file-option"
diff --git a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
index 1b59777f901..254bc235691 100644
--- a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
+++ b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
@@ -3,6 +3,7 @@ import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import { pluralize, truncate } from '~/lib/utils/text_utility';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import { COUNT_OF_AVATARS_IN_GUTTER, LENGTH_OF_AVATAR_TOOLTIP } from '../constants';
export default {
@@ -10,6 +11,9 @@ export default {
Icon,
UserAvatarImage,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
discussions: {
type: Array,
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 6eff3013dcd..8f037eeefc4 100644
--- a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
@@ -73,7 +73,7 @@ export default {
}),
...mapGetters(['isLoggedIn']),
lineHref() {
- return `#${this.line.lineCode || ''}`;
+ return `#${this.line.line_code || ''}`;
},
shouldShowCommentButton() {
return (
@@ -99,7 +99,7 @@ export default {
methods: {
...mapActions('diffs', ['loadMoreLines', 'showCommentForm']),
handleCommentButton() {
- this.showCommentForm({ lineCode: this.line.lineCode });
+ this.showCommentForm({ lineCode: this.line.line_code });
},
handleLoadMoreLines() {
if (this.isRequesting) {
@@ -108,8 +108,8 @@ export default {
this.isRequesting = true;
const endpoint = this.contextLinesPath;
- const oldLineNumber = this.line.metaData.oldPos || 0;
- const newLineNumber = this.line.metaData.newPos || 0;
+ const oldLineNumber = this.line.meta_data.old_pos || 0;
+ const newLineNumber = this.line.meta_data.new_pos || 0;
const offset = newLineNumber - oldLineNumber;
const bottom = this.isBottom;
const { fileHash } = this;
@@ -125,12 +125,12 @@ export default {
to = lineNumber + UNFOLD_COUNT;
} else {
const diffFile = utils.findDiffFile(this.diffFiles, this.fileHash);
- const indexForInline = utils.findIndexInInlineLines(diffFile.highlightedDiffLines, {
+ const indexForInline = utils.findIndexInInlineLines(diffFile.highlighted_diff_lines, {
oldLineNumber,
newLineNumber,
});
- const prevLine = diffFile.highlightedDiffLines[indexForInline - 2];
- const prevLineNumber = (prevLine && prevLine.newLine) || 0;
+ const prevLine = diffFile.highlighted_diff_lines[indexForInline - 2];
+ const prevLineNumber = (prevLine && prevLine.new_line) || 0;
if (since <= prevLineNumber + 1) {
since = prevLineNumber + 1;
@@ -167,7 +167,7 @@ export default {
<button
v-if="shouldShowCommentButton"
type="button"
- class="add-diff-note js-add-diff-note-button"
+ class="add-diff-note js-add-diff-note-button qa-diff-comment"
title="Add a comment to this line"
@click="handleCommentButton"
>
diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
index bb9bb821de3..07f38172575 100644
--- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
@@ -53,7 +53,7 @@ export default {
this.noteableData.diff_head_sha,
DIFF_NOTE_TYPE,
this.noteableData.source_project_id,
- this.line.lineCode,
+ this.line.line_code,
];
this.initAutoSave(this.noteableData, keys);
@@ -72,7 +72,7 @@ export default {
}
this.cancelCommentForm({
- lineCode: this.line.lineCode,
+ lineCode: this.line.line_code,
});
this.$nextTick(() => {
this.resetAutoSave();
@@ -94,7 +94,7 @@ export default {
<note-form
ref="noteForm"
:is-editing="true"
- :line-code="line.lineCode"
+ :line-code="line.line_code"
save-button-title="Comment"
class="diff-comment-form"
@cancelForm="handleCancelCommentForm"
diff --git a/app/assets/javascripts/diffs/components/diff_table_cell.vue b/app/assets/javascripts/diffs/components/diff_table_cell.vue
index 5d9a0b123fe..0a893a57f07 100644
--- a/app/assets/javascripts/diffs/components/diff_table_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_table_cell.vue
@@ -96,9 +96,7 @@ export default {
};
},
lineNumber() {
- const { lineType } = this;
-
- return lineType === OLD_LINE_TYPE ? this.line.oldLine : this.line.newLine;
+ return this.lineType === OLD_LINE_TYPE ? this.line.old_line : this.line.new_line;
},
},
};
diff --git a/app/assets/javascripts/diffs/components/image_diff_overlay.vue b/app/assets/javascripts/diffs/components/image_diff_overlay.vue
new file mode 100644
index 00000000000..ae1b0a52901
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/image_diff_overlay.vue
@@ -0,0 +1,139 @@
+<script>
+import { mapActions, mapGetters } from 'vuex';
+import _ from 'underscore';
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ name: 'ImageDiffOverlay',
+ components: {
+ Icon,
+ },
+ props: {
+ discussions: {
+ type: [Array, Object],
+ required: true,
+ },
+ fileHash: {
+ type: String,
+ required: true,
+ },
+ canComment: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showCommentIcon: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ badgeClass: {
+ type: String,
+ required: false,
+ default: 'badge badge-pill',
+ },
+ shouldToggleDiscussion: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ computed: {
+ ...mapGetters('diffs', ['getDiffFileByHash', 'getCommentFormForDiffFile']),
+ currentCommentForm() {
+ return this.getCommentFormForDiffFile(this.fileHash);
+ },
+ allDiscussions() {
+ return _.isArray(this.discussions) ? this.discussions : [this.discussions];
+ },
+ },
+ methods: {
+ ...mapActions(['toggleDiscussion']),
+ ...mapActions('diffs', ['openDiffFileCommentForm']),
+ getImageDimensions() {
+ return {
+ width: this.$parent.width,
+ height: this.$parent.height,
+ };
+ },
+ getPositionForObject(meta) {
+ const { x, y, width, height } = meta;
+ const imageWidth = this.getImageDimensions().width;
+ const imageHeight = this.getImageDimensions().height;
+ const widthRatio = imageWidth / width;
+ const heightRatio = imageHeight / height;
+
+ return {
+ x: Math.round(x * widthRatio),
+ y: Math.round(y * heightRatio),
+ };
+ },
+ getPosition(discussion) {
+ const { x, y } = this.getPositionForObject(discussion.position);
+
+ return {
+ left: `${x}px`,
+ top: `${y}px`,
+ };
+ },
+ clickedImage(x, y) {
+ const { width, height } = this.getImageDimensions();
+
+ this.openDiffFileCommentForm({
+ fileHash: this.fileHash,
+ width,
+ height,
+ x,
+ y,
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="position-absolute w-100 h-100 image-diff-overlay">
+ <button
+ v-if="canComment"
+ type="button"
+ class="btn-transparent position-absolute image-diff-overlay-add-comment w-100 h-100 js-add-image-diff-note-button"
+ @click="clickedImage($event.offsetX, $event.offsetY)"
+ >
+ <span class="sr-only">
+ {{ __('Add image comment') }}
+ </span>
+ </button>
+ <button
+ v-for="(discussion, index) in allDiscussions"
+ :key="discussion.id"
+ :style="getPosition(discussion)"
+ :class="badgeClass"
+ :disabled="!shouldToggleDiscussion"
+ class="js-image-badge"
+ type="button"
+ @click="toggleDiscussion({ discussionId: discussion.id })"
+ >
+ <icon
+ v-if="showCommentIcon"
+ name="image-comment-dark"
+ />
+ <template v-else>
+ {{ index + 1 }}
+ </template>
+ </button>
+ <button
+ v-if="currentCommentForm"
+ :style="{
+ left: `${currentCommentForm.x}px`,
+ top: `${currentCommentForm.y}px`
+ }"
+ :aria-label="__('Comment form position')"
+ class="btn-transparent comment-indicator"
+ type="button"
+ >
+ <icon
+ name="image-comment-dark"
+ />
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
index 46a51859da5..b9e14c53d2c 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
@@ -48,7 +48,7 @@ export default {
:discussions="line.discussions"
/>
<diff-line-note-form
- v-if="diffLineCommentForms[line.lineCode]"
+ v-if="diffLineCommentForms[line.line_code]"
:diff-file-hash="diffFileHash"
:line="line"
:note-target-line="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 62fa34e835a..1f4088066d1 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
@@ -52,9 +52,7 @@ export default {
};
},
inlineRowId() {
- const { lineCode, oldLine, newLine } = this.line;
-
- return lineCode || `${this.fileHash}_${oldLine}_${newLine}`;
+ return this.line.line_code || `${this.fileHash}_${this.line.old_line}_${this.line.new_line}`;
},
},
created() {
@@ -102,12 +100,12 @@ export default {
:line-type="newLineType"
:is-bottom="isBottom"
:is-hover="isHover"
- class="diff-line-num new_line"
+ class="diff-line-num new_line qa-new-diff-line"
/>
<td
:class="line.type"
class="line_content"
- v-html="line.richText"
+ v-html="line.rich_text"
>
</td>
</tr>
diff --git a/app/assets/javascripts/diffs/components/inline_diff_view.vue b/app/assets/javascripts/diffs/components/inline_diff_view.vue
index fbf9e77ac07..79efac89e98 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_view.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_view.vue
@@ -43,16 +43,16 @@ export default {
v-for="(line, index) in diffLines"
>
<inline-diff-table-row
- :key="line.lineCode"
- :file-hash="diffFile.fileHash"
- :context-lines-path="diffFile.contextLinesPath"
+ :key="line.line_code"
+ :file-hash="diffFile.file_hash"
+ :context-lines-path="diffFile.context_lines_path"
:line="line"
:is-bottom="index + 1 === diffLinesLength"
/>
<inline-diff-comment-row
v-if="shouldRenderInlineCommentRow(line)"
:key="index"
- :diff-file-hash="diffFile.fileHash"
+ :diff-file-hash="diffFile.file_hash"
:line="line"
:line-index="index"
/>
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
index 3339c56cbb6..00c2df4dac1 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
@@ -27,10 +27,10 @@ export default {
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}),
leftLineCode() {
- return this.line.left && this.line.left.lineCode;
+ return this.line.left && this.line.left.line_code;
},
rightLineCode() {
- return this.line.right && this.line.right.lineCode;
+ return this.line.right && this.line.right.line_code;
},
hasExpandedDiscussionOnLeft() {
return this.line.left && this.line.left.discussions
@@ -76,8 +76,9 @@ export default {
:class="className"
class="notes_holder"
>
- <td class="notes_line old"></td>
- <td class="notes_content parallel old">
+ <td
+ class="notes_content parallel old"
+ colspan="2">
<div
v-if="shouldRenderDiscussionsOnLeft"
class="content"
@@ -95,8 +96,9 @@ export default {
line-position="left"
/>
</td>
- <td class="notes_line new"></td>
- <td class="notes_content parallel new">
+ <td
+ class="notes_content parallel new"
+ colspan="2">
<div
v-if="shouldRenderDiscussionsOnRight"
class="content"
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
index fcc3b3e9117..2d87db12fd6 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
@@ -120,11 +120,11 @@ export default {
class="diff-line-num old_line"
/>
<td
- :id="line.left.lineCode"
+ :id="line.left.line_code"
:class="parallelViewLeftLineType"
class="line_content parallel left-side"
@mousedown.native="handleParallelLineMouseDown"
- v-html="line.left.richText"
+ v-html="line.left.rich_text"
>
</td>
</template>
@@ -146,11 +146,11 @@ export default {
class="diff-line-num new_line"
/>
<td
- :id="line.right.lineCode"
+ :id="line.right.line_code"
:class="line.right.type"
class="line_content parallel right-side"
@mousedown.native="handleParallelLineMouseDown"
- v-html="line.right.richText"
+ v-html="line.right.rich_text"
>
</td>
</template>
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_view.vue b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
index 3452f0d2b00..6942f9b53e0 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_view.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
@@ -46,8 +46,8 @@ export default {
>
<parallel-diff-table-row
:key="index"
- :file-hash="diffFile.fileHash"
- :context-lines-path="diffFile.contextLinesPath"
+ :file-hash="diffFile.file_hash"
+ :context-lines-path="diffFile.context_lines_path"
:line="line"
:is-bottom="index + 1 === diffLinesLength"
/>
@@ -55,7 +55,7 @@ export default {
v-if="shouldRenderParallelCommentRow(line)"
:key="`dcr-${index}`"
:line="line"
- :diff-file-hash="diffFile.fileHash"
+ :diff-file-hash="diffFile.file_hash"
:line-index="index"
/>
</template>
diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue
index 96e7bd63183..ff1eb23cea3 100644
--- a/app/assets/javascripts/diffs/components/tree_list.vue
+++ b/app/assets/javascripts/diffs/components/tree_list.vue
@@ -18,8 +18,8 @@ export default {
},
data() {
const treeListStored = localStorage.getItem(treeListStorageKey);
- const renderTreeList = treeListStored !== null ?
- convertPermissionToBoolean(treeListStored) : true;
+ const renderTreeList =
+ treeListStored !== null ? convertPermissionToBoolean(treeListStored) : true;
return {
search: '',
@@ -35,7 +35,7 @@ export default {
if (search === '') return this.renderTreeList ? this.tree : this.allBlobs;
- return this.allBlobs.filter(f => f.name.toLowerCase().indexOf(search) >= 0);
+ return this.allBlobs.filter(f => f.path.toLowerCase().indexOf(search) >= 0);
},
rowDisplayTextKey() {
if (this.renderTreeList && this.search.trim() === '') {
diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js
index 6a50d2c1426..78a39baa4cb 100644
--- a/app/assets/javascripts/diffs/constants.js
+++ b/app/assets/javascripts/diffs/constants.js
@@ -12,6 +12,7 @@ export const NOTE_TYPE = 'Note';
export const NEW_LINE_TYPE = 'new';
export const OLD_LINE_TYPE = 'old';
export const TEXT_DIFF_POSITION_TYPE = 'text';
+export const IMAGE_DIFF_POSITION_TYPE = 'image';
export const LINE_POSITION_LEFT = 'left';
export const LINE_POSITION_RIGHT = 'right';
diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js
index aae89109c27..06ef4207d85 100644
--- a/app/assets/javascripts/diffs/index.js
+++ b/app/assets/javascripts/diffs/index.js
@@ -1,6 +1,5 @@
import Vue from 'vue';
import { mapState } from 'vuex';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import diffsApp from './components/app.vue';
export default function initDiffsApp(store) {
@@ -17,9 +16,7 @@ export default function initDiffsApp(store) {
return {
endpoint: dataset.endpoint,
projectPath: dataset.projectPath,
- currentUser: convertObjectPropsToCamelCase(JSON.parse(dataset.currentUserData), {
- deep: true,
- }),
+ currentUser: JSON.parse(dataset.currentUserData) || {},
};
},
computed: {
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index ca8ae605cb4..fb648527450 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -50,8 +50,8 @@ export const assignDiscussionsToDiff = (
};
export const removeDiscussionsFromDiff = ({ commit }, removeDiscussion) => {
- const { fileHash, line_code } = removeDiscussion;
- commit(types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, { fileHash, lineCode: line_code });
+ const { file_hash, line_code, id } = removeDiscussion;
+ commit(types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, { fileHash: file_hash, lineCode: line_code, id });
};
export const startRenderDiffsQueue = ({ state, commit }) => {
@@ -189,6 +189,8 @@ export const saveDiffDiscussion = ({ dispatch }, { note, formData }) => {
return dispatch('saveNote', postData, { root: true })
.then(result => dispatch('updateDiscussion', result.discussion, { root: true }))
.then(discussion => dispatch('assignDiscussionsToDiff', [discussion]))
+ .then(() => dispatch('closeDiffFileCommentForm', formData.diffFile.file_hash))
+ .then(() => dispatch('startTaskList', null, { root: true }))
.catch(() => createFlash(s__('MergeRequests|Saving the comment failed')));
};
@@ -210,5 +212,19 @@ export const toggleShowTreeList = ({ commit, state }) => {
localStorage.setItem(MR_TREE_SHOW_KEY, state.showTreeList);
};
+export const openDiffFileCommentForm = ({ commit, getters }, formData) => {
+ const form = getters.getCommentFormForDiffFile(formData.fileHash);
+
+ if (form) {
+ commit(types.UPDATE_DIFF_FILE_COMMENT_FORM, formData);
+ } else {
+ commit(types.OPEN_DIFF_FILE_COMMENT_FORM, formData);
+ }
+};
+
+export const closeDiffFileCommentForm = ({ commit }, fileHash) => {
+ commit(types.CLOSE_DIFF_FILE_COMMENT_FORM, fileHash);
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index d4c205882ff..7f02c67a64e 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -1,11 +1,10 @@
-import _ from 'underscore';
import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '../constants';
export const isParallelView = state => state.diffViewType === PARALLEL_DIFF_VIEW_TYPE;
export const isInlineView = state => state.diffViewType === INLINE_DIFF_VIEW_TYPE;
-export const areAllFilesCollapsed = state => state.diffFiles.every(file => file.collapsed);
+export const hasCollapsedFile = state => state.diffFiles.some(file => file.collapsed);
export const commitId = state => (state.commit && state.commit.id ? state.commit.id : null);
@@ -68,8 +67,7 @@ export const diffHasDiscussions = (state, getters) => diff =>
*/
export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) => diff =>
rootGetters.discussions.filter(
- discussion =>
- discussion.diff_discussion && _.isEqual(discussion.diff_file.file_hash, diff.fileHash),
+ discussion => discussion.diff_discussion && discussion.diff_file.file_hash === diff.file_hash,
) || [];
export const shouldRenderParallelCommentRow = state => line => {
@@ -90,14 +88,14 @@ export const shouldRenderParallelCommentRow = state => line => {
return true;
}
- const hasCommentFormOnLeft = line.left && state.diffLineCommentForms[line.left.lineCode];
- const hasCommentFormOnRight = line.right && state.diffLineCommentForms[line.right.lineCode];
+ const hasCommentFormOnLeft = line.left && state.diffLineCommentForms[line.left.line_code];
+ const hasCommentFormOnRight = line.right && state.diffLineCommentForms[line.right.line_code];
return hasCommentFormOnLeft || hasCommentFormOnRight;
};
export const shouldRenderInlineCommentRow = state => line => {
- if (state.diffLineCommentForms[line.lineCode]) return true;
+ if (state.diffLineCommentForms[line.line_code]) return true;
if (!line.discussions || line.discussions.length === 0) {
return false;
@@ -108,11 +106,14 @@ export const shouldRenderInlineCommentRow = state => line => {
// prevent babel-plugin-rewire from generating an invalid default during karma∂ tests
export const getDiffFileByHash = state => fileHash =>
- state.diffFiles.find(file => file.fileHash === fileHash);
+ state.diffFiles.find(file => file.file_hash === fileHash);
export const allBlobs = state => Object.values(state.treeEntries).filter(f => f.type === 'blob');
export const diffFilesLength = state => state.diffFiles.length;
+export const getCommentFormForDiffFile = state => fileHash =>
+ state.commentForms.find(form => form.fileHash === fileHash);
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js
index 1c5c35071de..085e255f1d3 100644
--- a/app/assets/javascripts/diffs/store/modules/diff_state.js
+++ b/app/assets/javascripts/diffs/store/modules/diff_state.js
@@ -24,4 +24,6 @@ export default () => ({
showTreeList:
storedTreeShow === null ? bp.getBreakpointSize() !== 'xs' : storedTreeShow === 'true',
currentDiffFileId: '',
+ projectPath: '',
+ commentForms: [],
});
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index 6474ee628e2..e011031e72c 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -14,3 +14,7 @@ export const REMOVE_LINE_DISCUSSIONS_FOR_FILE = 'REMOVE_LINE_DISCUSSIONS_FOR_FIL
export const TOGGLE_FOLDER_OPEN = 'TOGGLE_FOLDER_OPEN';
export const TOGGLE_SHOW_TREE_LIST = 'TOGGLE_SHOW_TREE_LIST';
export const UPDATE_CURRENT_DIFF_FILE_ID = 'UPDATE_CURRENT_DIFF_FILE_ID';
+
+export const OPEN_DIFF_FILE_COMMENT_FORM = 'OPEN_DIFF_FILE_COMMENT_FORM';
+export const UPDATE_DIFF_FILE_COMMENT_FORM = 'UPDATE_DIFF_FILE_COMMENT_FORM';
+export const CLOSE_DIFF_FILE_COMMENT_FORM = 'CLOSE_DIFF_FILE_COMMENT_FORM';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 38a65f111a2..2133cfe4825 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -23,12 +23,11 @@ export default {
},
[types.SET_DIFF_DATA](state, data) {
- const diffData = convertObjectPropsToCamelCase(data, { deep: true });
- prepareDiffData(diffData);
- const { tree, treeEntries } = generateTreeList(diffData.diffFiles);
+ prepareDiffData(data);
+ const { tree, treeEntries } = generateTreeList(data.diff_files);
Object.assign(state, {
- ...diffData,
+ ...convertObjectPropsToCamelCase(data),
tree: sortTree(tree),
treeEntries,
});
@@ -42,7 +41,7 @@ export default {
[types.SET_MERGE_REQUEST_DIFFS](state, mergeRequestDiffs) {
Object.assign(state, {
- mergeRequestDiffs: convertObjectPropsToCamelCase(mergeRequestDiffs, { deep: true }),
+ mergeRequestDiffs,
});
},
@@ -62,13 +61,18 @@ export default {
const { lineNumbers, contextLines, fileHash } = options;
const { bottom } = options.params;
const diffFile = findDiffFile(state.diffFiles, fileHash);
- const { highlightedDiffLines, parallelDiffLines } = diffFile;
removeMatchLine(diffFile, lineNumbers, bottom);
- const lines = addLineReferences(contextLines, lineNumbers, bottom);
+
+ const lines = addLineReferences(contextLines, lineNumbers, bottom).map(line => ({
+ ...line,
+ line_code: line.line_code || `${fileHash}_${line.old_line}_${line.new_line}`,
+ discussions: line.discussions || [],
+ }));
+
addContextLines({
- inlineLines: highlightedDiffLines,
- parallelLines: parallelDiffLines,
+ inlineLines: diffFile.highlighted_diff_lines,
+ parallelLines: diffFile.parallel_diff_lines,
contextLines: lines,
bottom,
lineNumbers,
@@ -76,10 +80,9 @@ export default {
},
[types.ADD_COLLAPSED_DIFFS](state, { file, data }) {
- const normalizedData = convertObjectPropsToCamelCase(data, { deep: true });
- prepareDiffData(normalizedData);
- const [newFileData] = normalizedData.diffFiles.filter(f => f.fileHash === file.fileHash);
- const selectedFile = state.diffFiles.find(f => f.fileHash === file.fileHash);
+ prepareDiffData(data);
+ const [newFileData] = data.diff_files.filter(f => f.file_hash === file.file_hash);
+ const selectedFile = state.diffFiles.find(f => f.file_hash === file.file_hash);
Object.assign(selectedFile, { ...newFileData });
},
@@ -95,20 +98,20 @@ export default {
const discussionLineCode = discussion.line_code;
const fileHash = discussion.diff_file.file_hash;
- const lineCheck = ({ lineCode }) =>
- lineCode === discussionLineCode &&
+ const lineCheck = line =>
+ line.line_code === discussionLineCode &&
isDiscussionApplicableToLine({
discussion,
- diffPosition: diffPositionByLineCode[lineCode],
+ diffPosition: diffPositionByLineCode[line.line_code],
latestDiff,
});
state.diffFiles = state.diffFiles.map(diffFile => {
- if (diffFile.fileHash === fileHash) {
+ if (diffFile.file_hash === fileHash) {
const file = { ...diffFile };
- if (file.highlightedDiffLines) {
- file.highlightedDiffLines = file.highlightedDiffLines.map(line => {
+ if (file.highlighted_diff_lines) {
+ file.highlighted_diff_lines = file.highlighted_diff_lines.map(line => {
if (lineCheck(line)) {
return {
...line,
@@ -120,8 +123,8 @@ export default {
});
}
- if (file.parallelDiffLines) {
- file.parallelDiffLines = file.parallelDiffLines.map(line => {
+ if (file.parallel_diff_lines) {
+ file.parallel_diff_lines = file.parallel_diff_lines.map(line => {
const left = line.left && lineCheck(line.left);
const right = line.right && lineCheck(line.right);
@@ -142,7 +145,7 @@ export default {
});
}
- if (!file.parallelDiffLines || !file.highlightedDiffLines) {
+ if (!file.parallel_diff_lines || !file.highlighted_diff_lines) {
file.discussions = file.discussions.concat(discussion);
}
@@ -153,25 +156,27 @@ export default {
});
},
- [types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode }) {
- const selectedFile = state.diffFiles.find(f => f.fileHash === fileHash);
+ [types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode, id }) {
+ const selectedFile = state.diffFiles.find(f => f.file_hash === fileHash);
if (selectedFile) {
- const targetLine = selectedFile.parallelDiffLines.find(
- line =>
- (line.left && line.left.lineCode === lineCode) ||
- (line.right && line.right.lineCode === lineCode),
- );
- if (targetLine) {
- const side = targetLine.left && targetLine.left.lineCode === lineCode ? 'left' : 'right';
-
- Object.assign(targetLine[side], {
- discussions: [],
- });
+ if (selectedFile.parallel_diff_lines) {
+ const targetLine = selectedFile.parallel_diff_lines.find(
+ line =>
+ (line.left && line.left.line_code === lineCode) ||
+ (line.right && line.right.line_code === lineCode),
+ );
+ if (targetLine) {
+ const side = targetLine.left && targetLine.left.line_code === lineCode ? 'left' : 'right';
+
+ Object.assign(targetLine[side], {
+ discussions: [],
+ });
+ }
}
- if (selectedFile.highlightedDiffLines) {
- const targetInlineLine = selectedFile.highlightedDiffLines.find(
- line => line.lineCode === lineCode,
+ if (selectedFile.highlighted_diff_lines) {
+ const targetInlineLine = selectedFile.highlighted_diff_lines.find(
+ line => line.line_code === lineCode,
);
if (targetInlineLine) {
@@ -180,6 +185,12 @@ export default {
});
}
}
+
+ if (selectedFile.discussions && selectedFile.discussions.length) {
+ selectedFile.discussions = selectedFile.discussions.filter(
+ discussion => discussion.id !== id,
+ );
+ }
}
},
[types.TOGGLE_FOLDER_OPEN](state, path) {
@@ -191,4 +202,25 @@ export default {
[types.UPDATE_CURRENT_DIFF_FILE_ID](state, fileId) {
state.currentDiffFileId = fileId;
},
+ [types.OPEN_DIFF_FILE_COMMENT_FORM](state, formData) {
+ state.commentForms.push({
+ ...formData,
+ });
+ },
+ [types.UPDATE_DIFF_FILE_COMMENT_FORM](state, formData) {
+ const { fileHash } = formData;
+
+ state.commentForms = state.commentForms.map(form => {
+ if (form.fileHash === fileHash) {
+ return {
+ ...formData,
+ };
+ }
+
+ return form;
+ });
+ },
+ [types.CLOSE_DIFF_FILE_COMMENT_FORM](state, fileHash) {
+ state.commentForms = state.commentForms.filter(form => form.fileHash !== fileHash);
+ },
};
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index a482a2b82c0..d9d3c0f2ca2 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -1,5 +1,5 @@
import _ from 'underscore';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { diffModes } from '~/ide/constants';
import {
LINE_POSITION_LEFT,
LINE_POSITION_RIGHT,
@@ -14,7 +14,7 @@ import {
} from '../constants';
export function findDiffFile(files, hash) {
- return files.filter(file => file.fileHash === hash)[0];
+ return files.filter(file => file.file_hash === hash)[0];
}
export const getReversePosition = linePosition => {
@@ -34,23 +34,28 @@ export function getFormData(params) {
noteTargetLine,
diffViewType,
linePosition,
+ positionType,
} = params;
const position = JSON.stringify({
- base_sha: diffFile.diffRefs.baseSha,
- start_sha: diffFile.diffRefs.startSha,
- head_sha: diffFile.diffRefs.headSha,
- old_path: diffFile.oldPath,
- new_path: diffFile.newPath,
- position_type: TEXT_DIFF_POSITION_TYPE,
- old_line: noteTargetLine.oldLine,
- new_line: noteTargetLine.newLine,
+ base_sha: diffFile.diff_refs.base_sha,
+ start_sha: diffFile.diff_refs.start_sha,
+ head_sha: diffFile.diff_refs.head_sha,
+ old_path: diffFile.old_path,
+ new_path: diffFile.new_path,
+ position_type: positionType || TEXT_DIFF_POSITION_TYPE,
+ old_line: noteTargetLine ? noteTargetLine.old_line : null,
+ new_line: noteTargetLine ? noteTargetLine.new_line : null,
+ x: params.x,
+ y: params.y,
+ width: params.width,
+ height: params.height,
});
const postData = {
view: diffViewType,
line_type: linePosition === LINE_POSITION_RIGHT ? NEW_LINE_TYPE : OLD_LINE_TYPE,
- merge_request_diff_head_sha: diffFile.diffRefs.headSha,
+ merge_request_diff_head_sha: diffFile.diff_refs.head_sha,
in_reply_to_discussion_id: '',
note_project_id: '',
target_type: noteableData.targetType,
@@ -63,10 +68,10 @@ export function getFormData(params) {
noteable_id: noteableData.id,
commit_id: '',
type:
- diffFile.diffRefs.startSha && diffFile.diffRefs.headSha
+ diffFile.diff_refs.start_sha && diffFile.diff_refs.head_sha
? DIFF_NOTE_TYPE
: LEGACY_DIFF_NOTE_TYPE,
- line_code: noteTargetLine.lineCode,
+ line_code: noteTargetLine ? noteTargetLine.line_code : null,
},
};
@@ -87,7 +92,7 @@ export const findIndexInInlineLines = (lines, lineNumbers) => {
return _.findIndex(
lines,
- line => line.oldLine === oldLineNumber && line.newLine === newLineNumber,
+ line => line.old_line === oldLineNumber && line.new_line === newLineNumber,
);
};
@@ -99,18 +104,18 @@ export const findIndexInParallelLines = (lines, lineNumbers) => {
line =>
line.left &&
line.right &&
- line.left.oldLine === oldLineNumber &&
- line.right.newLine === newLineNumber,
+ line.left.old_line === oldLineNumber &&
+ line.right.new_line === newLineNumber,
);
};
export function removeMatchLine(diffFile, lineNumbers, bottom) {
- const indexForInline = findIndexInInlineLines(diffFile.highlightedDiffLines, lineNumbers);
- const indexForParallel = findIndexInParallelLines(diffFile.parallelDiffLines, lineNumbers);
+ const indexForInline = findIndexInInlineLines(diffFile.highlighted_diff_lines, lineNumbers);
+ const indexForParallel = findIndexInParallelLines(diffFile.parallel_diff_lines, lineNumbers);
const factor = bottom ? 1 : -1;
- diffFile.highlightedDiffLines.splice(indexForInline + factor, 1);
- diffFile.parallelDiffLines.splice(indexForParallel + factor, 1);
+ diffFile.highlighted_diff_lines.splice(indexForInline + factor, 1);
+ diffFile.parallel_diff_lines.splice(indexForParallel + factor, 1);
}
export function addLineReferences(lines, lineNumbers, bottom) {
@@ -119,18 +124,16 @@ export function addLineReferences(lines, lineNumbers, bottom) {
let matchLineIndex = -1;
const linesWithNumbers = lines.map((l, index) => {
- const line = convertObjectPropsToCamelCase(l);
-
- if (line.type === MATCH_LINE_TYPE) {
+ if (l.type === MATCH_LINE_TYPE) {
matchLineIndex = index;
} else {
- Object.assign(line, {
- oldLine: bottom ? oldLineNumber + index + 1 : oldLineNumber + index - lineCount,
- newLine: bottom ? newLineNumber + index + 1 : newLineNumber + index - lineCount,
+ Object.assign(l, {
+ old_line: bottom ? oldLineNumber + index + 1 : oldLineNumber + index - lineCount,
+ new_line: bottom ? newLineNumber + index + 1 : newLineNumber + index - lineCount,
});
}
- return line;
+ return l;
});
if (matchLineIndex > -1) {
@@ -140,9 +143,9 @@ export function addLineReferences(lines, lineNumbers, bottom) {
: linesWithNumbers[matchLineIndex + 1];
Object.assign(line, {
- metaData: {
- oldPos: targetLine.oldLine,
- newPos: targetLine.newLine,
+ meta_data: {
+ old_pos: targetLine.old_line,
+ new_pos: targetLine.new_line,
},
});
}
@@ -181,11 +184,11 @@ export function trimFirstCharOfLineContent(line = {}) {
const parsedLine = Object.assign({}, line);
- if (line.richText) {
- const firstChar = parsedLine.richText.charAt(0);
+ if (line.rich_text) {
+ const firstChar = parsedLine.rich_text.charAt(0);
if (firstChar === ' ' || firstChar === '+' || firstChar === '-') {
- parsedLine.richText = line.richText.substring(1);
+ parsedLine.rich_text = line.rich_text.substring(1);
}
}
@@ -195,15 +198,15 @@ export function trimFirstCharOfLineContent(line = {}) {
// This prepares and optimizes the incoming diff data from the server
// by setting up incremental rendering and removing unneeded data
export function prepareDiffData(diffData) {
- const filesLength = diffData.diffFiles.length;
+ const filesLength = diffData.diff_files.length;
let showingLines = 0;
for (let i = 0; i < filesLength; i += 1) {
- const file = diffData.diffFiles[i];
+ const file = diffData.diff_files[i];
- if (file.parallelDiffLines) {
- const linesLength = file.parallelDiffLines.length;
+ if (file.parallel_diff_lines) {
+ const linesLength = file.parallel_diff_lines.length;
for (let u = 0; u < linesLength; u += 1) {
- const line = file.parallelDiffLines[u];
+ const line = file.parallel_diff_lines[u];
if (line.left) {
line.left = trimFirstCharOfLineContent(line.left);
}
@@ -213,44 +216,40 @@ export function prepareDiffData(diffData) {
}
}
- if (file.highlightedDiffLines) {
- const linesLength = file.highlightedDiffLines.length;
+ if (file.highlighted_diff_lines) {
+ const linesLength = file.highlighted_diff_lines.length;
for (let u = 0; u < linesLength; u += 1) {
- const line = file.highlightedDiffLines[u];
+ const line = file.highlighted_diff_lines[u];
Object.assign(line, { ...trimFirstCharOfLineContent(line) });
}
- showingLines += file.parallelDiffLines.length;
+ showingLines += file.parallel_diff_lines.length;
}
Object.assign(file, {
renderIt: showingLines < LINES_TO_BE_RENDERED_DIRECTLY,
collapsed: file.text && showingLines > MAX_LINES_TO_BE_RENDERED,
+ discussions: [],
});
}
}
export function getDiffPositionByLineCode(diffFiles) {
return diffFiles.reduce((acc, diffFile) => {
- const { baseSha, headSha, startSha } = diffFile.diffRefs;
- const { newPath, oldPath } = diffFile;
-
// We can only use highlightedDiffLines to create the map of diff lines because
// highlightedDiffLines will also include every parallel diff line in it.
- if (diffFile.highlightedDiffLines) {
- diffFile.highlightedDiffLines.forEach(line => {
- const { lineCode, oldLine, newLine } = line;
-
- if (lineCode) {
- acc[lineCode] = {
- baseSha,
- headSha,
- startSha,
- newPath,
- oldPath,
- oldLine,
- newLine,
- lineCode,
- positionType: 'text',
+ if (diffFile.highlighted_diff_lines) {
+ diffFile.highlighted_diff_lines.forEach(line => {
+ if (line.line_code) {
+ acc[line.line_code] = {
+ base_sha: diffFile.diff_refs.base_sha,
+ head_sha: diffFile.diff_refs.head_sha,
+ start_sha: diffFile.diff_refs.start_sha,
+ new_path: diffFile.new_path,
+ old_path: diffFile.old_path,
+ old_line: line.old_line,
+ new_line: line.new_line,
+ line_code: line.line_code,
+ position_type: 'text',
};
}
});
@@ -263,30 +262,30 @@ export function getDiffPositionByLineCode(diffFiles) {
// This method will check whether the discussion is still applicable
// to the diff line in question regarding different versions of the MR
export function isDiscussionApplicableToLine({ discussion, diffPosition, latestDiff }) {
- const { lineCode, ...diffPositionCopy } = diffPosition;
+ const { line_code, ...diffPositionCopy } = diffPosition;
if (discussion.original_position && discussion.position) {
- const originalRefs = convertObjectPropsToCamelCase(discussion.original_position);
- const refs = convertObjectPropsToCamelCase(discussion.position);
+ const originalRefs = discussion.original_position;
+ const refs = discussion.position;
return _.isEqual(refs, diffPositionCopy) || _.isEqual(originalRefs, diffPositionCopy);
}
- return latestDiff && discussion.active && lineCode === discussion.line_code;
+ // eslint-disable-next-line
+ return latestDiff && discussion.active && line_code === discussion.line_code;
}
export const generateTreeList = files =>
files.reduce(
(acc, file) => {
- const { fileHash, addedLines, removedLines, newFile, deletedFile, newPath } = file;
- const split = newPath.split('/');
+ const split = file.new_path.split('/');
split.forEach((name, i) => {
const parent = acc.treeEntries[split.slice(0, i).join('/')];
const path = `${parent ? `${parent.path}/` : ''}${name}`;
if (!acc.treeEntries[path]) {
- const type = path === newPath ? 'blob' : 'tree';
+ const type = path === file.new_path ? 'blob' : 'tree';
acc.treeEntries[path] = {
key: path,
path,
@@ -300,11 +299,11 @@ export const generateTreeList = files =>
if (type === 'blob') {
Object.assign(entry, {
changed: true,
- tempFile: newFile,
- deleted: deletedFile,
- fileHash,
- addedLines,
- removedLines,
+ tempFile: file.new_file,
+ deleted: file.deleted_file,
+ fileHash: file.file_hash,
+ addedLines: file.added_lines,
+ removedLines: file.removed_lines,
});
} else {
Object.assign(entry, {
@@ -320,3 +319,8 @@ export const generateTreeList = files =>
},
{ treeEntries: {}, tree: [] },
);
+
+export const getDiffMode = diffFile => {
+ const diffModeKey = Object.keys(diffModes).find(key => diffFile[`${key}_file`]);
+ return diffModes[diffModeKey] || diffModes.replaced;
+};
diff --git a/app/assets/javascripts/dirty_submit/dirty_submit_form.js b/app/assets/javascripts/dirty_submit/dirty_submit_form.js
index 5bea47f23c5..d8d0fa1fac4 100644
--- a/app/assets/javascripts/dirty_submit/dirty_submit_form.js
+++ b/app/assets/javascripts/dirty_submit/dirty_submit_form.js
@@ -31,7 +31,7 @@ class DirtySubmitForm {
updateDirtyInput(event) {
const input = event.target;
- if (!input.dataset.dirtySubmitOriginalValue) return;
+ if (!input.dataset.isDirtySubmitInput) return;
this.updateDirtyInputs(input);
this.toggleSubmission();
@@ -65,6 +65,7 @@ class DirtySubmitForm {
}
static initInput(element) {
+ element.dataset.isDirtySubmitInput = true;
element.dataset.dirtySubmitOriginalValue = DirtySubmitForm.inputCurrentValue(element);
}
diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue
index 00d197d294f..a48f5fcb7d6 100644
--- a/app/assets/javascripts/environments/components/container.vue
+++ b/app/assets/javascripts/environments/components/container.vue
@@ -1,4 +1,5 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
import environmentTable from '../components/environments_table.vue';
@@ -6,6 +7,7 @@ export default {
components: {
environmentTable,
tablePagination,
+ GlLoadingIcon,
},
props: {
isLoading: {
diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue
index 2bc168a6b02..03c3ad0401f 100644
--- a/app/assets/javascripts/environments/components/environment_actions.vue
+++ b/app/assets/javascripts/environments/components/environment_actions.vue
@@ -1,7 +1,10 @@
<script>
+import { s__, sprintf } from '~/locale';
+import { formatTime } from '~/lib/utils/datetime_utility';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub';
import tooltip from '../../vue_shared/directives/tooltip';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
directives: {
@@ -9,6 +12,7 @@ export default {
},
components: {
Icon,
+ GlLoadingIcon,
},
props: {
actions: {
@@ -28,10 +32,24 @@ export default {
},
},
methods: {
- onClickAction(endpoint) {
+ onClickAction(action) {
+ if (action.scheduledAt) {
+ const confirmationMessage = sprintf(
+ s__(
+ "DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after it's timer finishes.",
+ ),
+ { jobName: action.name },
+ );
+ // https://gitlab.com/gitlab-org/gitlab-ce/issues/52156
+ // eslint-disable-next-line no-alert
+ if (!window.confirm(confirmationMessage)) {
+ return;
+ }
+ }
+
this.isLoading = true;
- eventHub.$emit('postAction', { endpoint });
+ eventHub.$emit('postAction', { endpoint: action.playPath });
},
isActionDisabled(action) {
@@ -41,6 +59,11 @@ export default {
return !action.playable;
},
+
+ remainingTime(action) {
+ const remainingMilliseconds = new Date(action.scheduledAt).getTime() - Date.now();
+ return formatTime(Math.max(0, remainingMilliseconds));
+ },
},
};
</script>
@@ -54,7 +77,7 @@ export default {
:aria-label="title"
:disabled="isLoading"
type="button"
- class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container"
+ class="dropdown btn btn-default dropdown-new js-environment-actions-dropdown"
data-container="body"
data-toggle="dropdown"
>
@@ -75,12 +98,19 @@ export default {
:class="{ disabled: isActionDisabled(action) }"
:disabled="isActionDisabled(action)"
type="button"
- class="js-manual-action-link no-btn btn"
- @click="onClickAction(action.play_path)"
+ class="js-manual-action-link no-btn btn d-flex align-items-center"
+ @click="onClickAction(action)"
>
- <span>
+ <span class="flex-fill">
{{ action.name }}
</span>
+ <span
+ v-if="action.scheduledAt"
+ class="text-secondary"
+ >
+ <icon name="clock" />
+ {{ remainingTime(action) }}
+ </span>
</button>
</li>
</ul>
diff --git a/app/assets/javascripts/environments/components/environment_external_url.vue b/app/assets/javascripts/environments/components/environment_external_url.vue
index 7446196de13..1e8a892c0b8 100644
--- a/app/assets/javascripts/environments/components/environment_external_url.vue
+++ b/app/assets/javascripts/environments/components/environment_external_url.vue
@@ -1,7 +1,7 @@
<script>
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
-import tooltip from '../../vue_shared/directives/tooltip';
-import { s__ } from '../../locale';
+import { s__ } from '~/locale';
/**
* Renders the external url link in environments table.
@@ -11,7 +11,7 @@ export default {
Icon,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
externalUrl: {
@@ -28,12 +28,11 @@ export default {
</script>
<template>
<a
- v-tooltip
+ v-gl-tooltip
:title="title"
:aria-label="title"
:href="externalUrl"
class="btn external-url"
- data-container="body"
target="_blank"
rel="noopener noreferrer nofollow"
>
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index bb9c139727e..50b0e9747ee 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -1,7 +1,7 @@
<script>
import Timeago from 'timeago.js';
import _ from 'underscore';
-import tooltip from '~/vue_shared/directives/tooltip';
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import { humanize } from '~/lib/utils/text_utility';
import Icon from '~/vue_shared/components/icon.vue';
@@ -13,9 +13,10 @@ import TerminalButtonComponent from './environment_terminal_button.vue';
import MonitoringButtonComponent from './environment_monitoring.vue';
import CommitComponent from '../../vue_shared/components/commit.vue';
import eventHub from '../event_hub';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
/**
- * Envrionment Item Component
+ * Environment Item Component
*
* Renders a table row for each environment.
*/
@@ -35,7 +36,7 @@ export default {
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
@@ -60,7 +61,7 @@ export default {
computed: {
/**
- * Verifies if `last_deployment` key exists in the current Envrionment.
+ * Verifies if `last_deployment` key exists in the current Environment.
* This key is required to render most of the html - this method works has
* an helper.
*
@@ -74,21 +75,6 @@ export default {
},
/**
- * Verifies is the given environment has manual actions.
- * Used to verify if we should render them or nor.
- *
- * @returns {Boolean|Undefined}
- */
- hasManualActions() {
- return (
- this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.manual_actions &&
- this.model.last_deployment.manual_actions.length > 0
- );
- },
-
- /**
* Checkes whether the environment is protected.
* (`is_protected` currently only set in EE)
*
@@ -154,23 +140,20 @@ export default {
return '';
},
- /**
- * Returns the manual actions with the name parsed.
- *
- * @returns {Array.<Object>|Undefined}
- */
- manualActions() {
- if (this.hasManualActions) {
- return this.model.last_deployment.manual_actions.map(action => {
- const parsedAction = {
- name: humanize(action.name),
- play_path: action.play_path,
- playable: action.playable,
- };
- return parsedAction;
- });
+ actions() {
+ if (!this.model || !this.model.last_deployment || !this.canCreateDeployment) {
+ return [];
}
- return [];
+
+ const { manualActions, scheduledActions } = convertObjectPropsToCamelCase(
+ this.model.last_deployment,
+ { deep: true },
+ );
+ const combinedActions = (manualActions || []).concat(scheduledActions || []);
+ return combinedActions.map(action => ({
+ ...action,
+ name: humanize(action.name),
+ }));
},
/**
@@ -443,7 +426,7 @@ export default {
displayEnvironmentActions() {
return (
- this.hasManualActions ||
+ this.actions.length > 0 ||
this.externalURL ||
this.monitoringUrl ||
this.canStopEnvironment ||
@@ -472,7 +455,7 @@ export default {
class="gl-responsive-table-row"
role="row">
<div
- v-tooltip
+ v-gl-tooltip
:title="model.name"
class="table-section section-wrap section-15 text-truncate"
role="gridcell"
@@ -619,8 +602,8 @@ export default {
/>
<actions-component
- v-if="hasManualActions && canCreateDeployment"
- :actions="manualActions"
+ v-if="actions.length > 0"
+ :actions="actions"
/>
<terminal-button-component
diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue
index 26bec125445..7c723fa8979 100644
--- a/app/assets/javascripts/environments/components/environment_monitoring.vue
+++ b/app/assets/javascripts/environments/components/environment_monitoring.vue
@@ -2,9 +2,8 @@
/**
* Renders the Monitoring (Metrics) link in environments table.
*/
-import { GlButton } from '@gitlab-org/gitlab-ui';
+import { GlButton, GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
-import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: {
@@ -12,7 +11,7 @@ export default {
GlButton,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
monitoringUrl: {
@@ -29,12 +28,11 @@ export default {
</script>
<template>
<gl-button
- v-tooltip
+ v-gl-tooltip
:href="monitoringUrl"
:title="title"
:aria-label="title"
class="monitoring-url d-none d-sm-none d-md-block"
- data-container="body"
rel="noopener noreferrer nofollow"
variant="default"
>
diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue
index 9e137f79dcc..298469e6482 100644
--- a/app/assets/javascripts/environments/components/environment_rollback.vue
+++ b/app/assets/javascripts/environments/components/environment_rollback.vue
@@ -6,19 +6,18 @@
* Makes a post request when the button is clicked.
*/
import { s__ } from '~/locale';
+import { GlTooltipDirective, GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
-import tooltip from '~/vue_shared/directives/tooltip';
import eventHub from '../event_hub';
export default {
components: {
Icon,
+ GlLoadingIcon,
},
-
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
-
props: {
retryUrl: {
type: String,
@@ -55,21 +54,21 @@ export default {
</script>
<template>
<button
- v-tooltip
+ v-gl-tooltip
:disabled="isLoading"
:title="title"
type="button"
class="btn d-none d-sm-none d-md-block"
@click="onClick"
>
-
<icon
v-if="isLastDeployment"
- name="repeat" />
+ name="repeat"
+ />
<icon
v-else
- name="redo"/>
-
+ name="redo"
+ />
<gl-loading-icon v-if="isLoading" />
</button>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_stop.vue b/app/assets/javascripts/environments/components/environment_stop.vue
index a814b3405f5..327c96a93e9 100644
--- a/app/assets/javascripts/environments/components/environment_stop.vue
+++ b/app/assets/javascripts/environments/components/environment_stop.vue
@@ -5,49 +5,42 @@
*/
import $ from 'jquery';
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
import LoadingButton from '../../vue_shared/components/loading_button.vue';
-import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: {
Icon,
LoadingButton,
},
-
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
-
props: {
environment: {
type: Object,
required: true,
},
},
-
data() {
return {
isLoading: false,
};
},
-
computed: {
title() {
return s__('Environments|Stop environment');
},
},
-
mounted() {
eventHub.$on('stopEnvironment', this.onStopEnvironment);
},
-
beforeDestroy() {
eventHub.$off('stopEnvironment', this.onStopEnvironment);
},
-
methods: {
onClick() {
$(this.$el).tooltip('dispose');
@@ -63,12 +56,11 @@ export default {
</script>
<template>
<loading-button
- v-tooltip
+ v-gl-tooltip
:loading="isLoading"
:title="title"
:aria-label="title"
container-class="btn btn-danger d-none d-sm-none d-md-block"
- data-container="body"
data-toggle="modal"
data-target="#stop-environment-modal"
@click="onClick"
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue
index 350417e5ad0..b8b909f350c 100644
--- a/app/assets/javascripts/environments/components/environment_terminal_button.vue
+++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue
@@ -3,15 +3,15 @@
* Renders a terminal button to open a web terminal.
* Used in environments table.
*/
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
-import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: {
Icon,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
terminalPath: {
@@ -29,12 +29,11 @@ export default {
</script>
<template>
<a
- v-tooltip
+ v-gl-tooltip
:title="title"
:aria-label="title"
:href="terminalPath"
class="btn terminal-button d-none d-sm-none d-md-block"
- data-container="body"
>
<icon name="terminal" />
</a>
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index e2ecf426e64..557b2062c64 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -1,94 +1,92 @@
<script>
- import Flash from '../../flash';
- import { s__ } from '../../locale';
- import emptyState from './empty_state.vue';
- import eventHub from '../event_hub';
- import environmentsMixin from '../mixins/environments_mixin';
- import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
- import StopEnvironmentModal from './stop_environment_modal.vue';
+import Flash from '../../flash';
+import { s__ } from '../../locale';
+import emptyState from './empty_state.vue';
+import eventHub from '../event_hub';
+import environmentsMixin from '../mixins/environments_mixin';
+import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
+import StopEnvironmentModal from './stop_environment_modal.vue';
- export default {
- components: {
- emptyState,
- StopEnvironmentModal,
- },
+export default {
+ components: {
+ emptyState,
+ StopEnvironmentModal,
+ },
- mixins: [
- CIPaginationMixin,
- environmentsMixin,
- ],
+ mixins: [CIPaginationMixin, environmentsMixin],
- props: {
- endpoint: {
- type: String,
- required: true,
- },
- canCreateEnvironment: {
- type: Boolean,
- required: true,
- },
- canCreateDeployment: {
- type: Boolean,
- required: true,
- },
- canReadEnvironment: {
- type: Boolean,
- required: true,
- },
- cssContainerClass: {
- type: String,
- required: true,
- },
- newEnvironmentPath: {
- type: String,
- required: true,
- },
- helpPagePath: {
- type: String,
- required: true,
- },
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
},
-
- created() {
- eventHub.$on('toggleFolder', this.toggleFolder);
+ canCreateEnvironment: {
+ type: Boolean,
+ required: true,
},
-
- beforeDestroy() {
- eventHub.$off('toggleFolder');
+ canCreateDeployment: {
+ type: Boolean,
+ required: true,
+ },
+ canReadEnvironment: {
+ type: Boolean,
+ required: true,
+ },
+ cssContainerClass: {
+ type: String,
+ required: true,
+ },
+ newEnvironmentPath: {
+ type: String,
+ required: true,
},
+ helpPagePath: {
+ type: String,
+ required: true,
+ },
+ },
+
+ created() {
+ eventHub.$on('toggleFolder', this.toggleFolder);
+ },
- methods: {
- toggleFolder(folder) {
- this.store.toggleFolder(folder);
+ beforeDestroy() {
+ eventHub.$off('toggleFolder');
+ },
- if (!folder.isOpen) {
- this.fetchChildEnvironments(folder, true);
- }
- },
+ methods: {
+ toggleFolder(folder) {
+ this.store.toggleFolder(folder);
- fetchChildEnvironments(folder, showLoader = false) {
- this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader);
+ if (!folder.isOpen) {
+ this.fetchChildEnvironments(folder, true);
+ }
+ },
- this.service.getFolderContent(folder.folder_path)
- .then(response => this.store.setfolderContent(folder, response.data.environments))
- .then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false))
- .catch(() => {
- Flash(s__('Environments|An error occurred while fetching the environments.'));
- this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false);
- });
- },
+ fetchChildEnvironments(folder, showLoader = false) {
+ this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader);
+
+ this.service
+ .getFolderContent(folder.folder_path)
+ .then(response => this.store.setfolderContent(folder, response.data.environments))
+ .then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false))
+ .catch(() => {
+ Flash(s__('Environments|An error occurred while fetching the environments.'));
+ this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false);
+ });
+ },
- successCallback(resp) {
- this.saveData(resp);
+ successCallback(resp) {
+ this.saveData(resp);
- // We need to verify if any folder is open to also update it
- const openFolders = this.store.getOpenFolders();
- if (openFolders.length) {
- openFolders.forEach(folder => this.fetchChildEnvironments(folder));
- }
- },
+ // We need to verify if any folder is open to also update it
+ const openFolders = this.store.getOpenFolders();
+ if (openFolders.length) {
+ openFolders.forEach(folder => this.fetchChildEnvironments(folder));
+ }
},
- };
+ },
+};
</script>
<template>
<div :class="cssContainerClass">
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index 16abafebbc0..c03d4f29ff9 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -2,11 +2,13 @@
/**
* Render environments table.
*/
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import environmentItem from './environment_item.vue';
export default {
components: {
environmentItem,
+ GlLoadingIcon,
},
props: {
diff --git a/app/assets/javascripts/environments/components/stop_environment_modal.vue b/app/assets/javascripts/environments/components/stop_environment_modal.vue
index 657cc8cd1aa..6397f6caf1b 100644
--- a/app/assets/javascripts/environments/components/stop_environment_modal.vue
+++ b/app/assets/javascripts/environments/components/stop_environment_modal.vue
@@ -1,7 +1,7 @@
<script>
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import GlModal from '~/vue_shared/components/gl_modal.vue';
import { s__, sprintf } from '~/locale';
-import tooltip from '~/vue_shared/directives/tooltip';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import eventHub from '../event_hub';
@@ -15,7 +15,7 @@ export default {
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
@@ -67,7 +67,7 @@ export default {
>
Stopping
<span
- v-tooltip
+ v-gl-tooltip
:title="environment.name"
class="text-truncate ml-1 mr-1 flex-fill"
>{{ environment.name }}</span>
diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js
index 5ce9225a4bb..5808a2d4afa 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -34,14 +34,14 @@ export default class EnvironmentsStore {
* @returns {Array}
*/
storeEnvironments(environments = []) {
- const filteredEnvironments = environments.map((env) => {
- const oldEnvironmentState = this.state.environments
- .find((element) => {
- if (env.latest) {
- return element.id === env.latest.id;
- }
- return element.id === env.id;
- }) || {};
+ const filteredEnvironments = environments.map(env => {
+ const oldEnvironmentState =
+ this.state.environments.find(element => {
+ if (env.latest) {
+ return element.id === env.latest.id;
+ }
+ return element.id === env.id;
+ }) || {};
let filtered = {};
@@ -101,11 +101,11 @@ export default class EnvironmentsStore {
}
/**
- * Toggles folder open property for the given folder.
- *
- * @param {Object} folder
- * @return {Array}
- */
+ * Toggles folder open property for the given folder.
+ *
+ * @param {Object} folder
+ * @return {Array}
+ */
toggleFolder(folder) {
return this.updateEnvironmentProp(folder, 'isOpen', !folder.isOpen);
}
@@ -119,7 +119,7 @@ export default class EnvironmentsStore {
* @return {Object}
*/
setfolderContent(folder, environments) {
- const updatedEnvironments = environments.map((env) => {
+ const updatedEnvironments = environments.map(env => {
let updated = env;
if (env.latest) {
@@ -148,7 +148,7 @@ export default class EnvironmentsStore {
updateEnvironmentProp(environment, prop, newValue) {
const { environments } = this.state;
- const updatedEnvironments = environments.map((env) => {
+ const updatedEnvironments = environments.map(env => {
const updateEnv = Object.assign({}, env);
if (env.id === environment.id) {
updateEnv[prop] = newValue;
diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js
index 3233f5c4f71..aad5647c045 100644
--- a/app/assets/javascripts/files_comment_button.js
+++ b/app/assets/javascripts/files_comment_button.js
@@ -1,5 +1,5 @@
/* Developer beware! Do not add logic to showButton or hideButton
- * that will force a reflow. Doing so will create a signficant performance
+ * that will force a reflow. Doing so will create a significant performance
* bottleneck for pages with large diffs. For a comprehensive list of what
* causes reflows, visit https://gist.github.com/paulirish/5d52fb081b3570c81e3a
*/
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js
index d36f38a70b5..d5027590bb7 100644
--- a/app/assets/javascripts/filtered_search/dropdown_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js
@@ -39,8 +39,9 @@ export default class DropdownUser extends FilteredSearchDropdown {
}
itemClicked(e) {
- super.itemClicked(e,
- selected => selected.querySelector('.dropdown-light-content').innerText.trim());
+ super.itemClicked(e, selected =>
+ selected.querySelector('.dropdown-light-content').innerText.trim(),
+ );
}
renderContent(forceShowList = false) {
@@ -68,7 +69,7 @@ export default class DropdownUser extends FilteredSearchDropdown {
// Removes the first character if it is a quotation so that we can search
// with multiple words
- if (value[0] === '"' || value[0] === '\'') {
+ if (value[0] === '"' || value[0] === "'") {
value = value.slice(1);
}
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
index 4eb67ff7649..146d3ba963c 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
@@ -85,7 +85,7 @@ export default class FilteredSearchDropdown {
}
dispatchInputEvent() {
- // Propogate input change to FilteredSearchDropdownManager
+ // Propagate input change to FilteredSearchDropdownManager
// so that it can determine which dropdowns to open
this.input.dispatchEvent(
new CustomEvent('input', {
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
index cd3d532c958..57ec6603d80 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -108,7 +108,7 @@ export default class FilteredSearchDropdownManager {
},
};
- supportedTokens.forEach((type) => {
+ supportedTokens.forEach(type => {
if (availableMappings[type]) {
allowedMappings[type] = availableMappings[type];
}
@@ -142,10 +142,7 @@ export default class FilteredSearchDropdownManager {
}
static addWordToInput(tokenName, tokenValue = '', clicked = false, options = {}) {
- const {
- uppercaseTokenName = false,
- capitalizeTokenValue = false,
- } = options;
+ const { uppercaseTokenName = false, capitalizeTokenValue = false } = options;
const input = FilteredSearchContainer.container.querySelector('.filtered-search');
FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue, {
uppercaseTokenName,
@@ -164,13 +161,16 @@ export default class FilteredSearchDropdownManager {
updateDropdownOffset(key) {
// Always align dropdown with the input field
- let offset = this.filteredSearchInput.getBoundingClientRect().left - this.container.querySelector('.scroll-container').getBoundingClientRect().left;
+ let offset =
+ this.filteredSearchInput.getBoundingClientRect().left -
+ this.container.querySelector('.scroll-container').getBoundingClientRect().left;
const maxInputWidth = 240;
const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth;
// Make sure offset never exceeds the input container
- const offsetMaxWidth = this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth;
+ const offsetMaxWidth =
+ this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth;
if (offsetMaxWidth < offset) {
offset = offsetMaxWidth;
}
@@ -196,8 +196,7 @@ export default class FilteredSearchDropdownManager {
const glArguments = Object.assign({}, defaultArguments, extraArguments);
// Passing glArguments to `new glClass(<arguments>)`
- mappingKey.reference =
- new (Function.prototype.bind.apply(glClass, [null, glArguments]))();
+ mappingKey.reference = new (Function.prototype.bind.apply(glClass, [null, glArguments]))();
}
if (firstLoad) {
@@ -224,8 +223,8 @@ export default class FilteredSearchDropdownManager {
}
const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase());
- const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key
- && this.mapping[match.key];
+ const shouldOpenFilterDropdown =
+ match && this.currentDropdown !== match.key && this.mapping[match.key];
const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint';
if (shouldOpenFilterDropdown || shouldOpenHintDropdown) {
@@ -236,8 +235,10 @@ export default class FilteredSearchDropdownManager {
setDropdown() {
const query = DropdownUtils.getSearchQuery(true);
- const { lastToken, searchToken } =
- this.tokenizer.processTokens(query, this.filteredSearchTokenKeys.getKeys());
+ const { lastToken, searchToken } = this.tokenizer.processTokens(
+ query,
+ this.filteredSearchTokenKeys.getKeys(),
+ );
if (this.currentDropdown) {
this.updateCurrentDropdownOffset();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 54533ebb70d..4a2af02b40a 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -1,8 +1,5 @@
import _ from 'underscore';
-import {
- getParameterByName,
- getUrlParamsArray,
-} from '~/lib/utils/common_utils';
+import { getParameterByName, getUrlParamsArray } from '~/lib/utils/common_utils';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import { visitUrl } from '../lib/utils/url_utility';
import Flash from '../flash';
@@ -48,24 +45,28 @@ export default class FilteredSearchManager {
isLocalStorageAvailable: RecentSearchesService.isAvailable(),
allowedKeys: this.filteredSearchTokenKeys.getKeys(),
});
- this.searchHistoryDropdownElement = document.querySelector('.js-filtered-search-history-dropdown');
- const fullPath = this.searchHistoryDropdownElement ?
- this.searchHistoryDropdownElement.dataset.fullPath : 'project';
+ this.searchHistoryDropdownElement = document.querySelector(
+ '.js-filtered-search-history-dropdown',
+ );
+ const fullPath = this.searchHistoryDropdownElement
+ ? this.searchHistoryDropdownElement.dataset.fullPath
+ : 'project';
const recentSearchesKey = `${fullPath}-${this.recentsStorageKeyNames[this.page]}`;
this.recentSearchesService = new RecentSearchesService(recentSearchesKey);
}
setup() {
// Fetch recent searches from localStorage
- this.fetchingRecentSearchesPromise = this.recentSearchesService.fetch()
- .catch((error) => {
+ this.fetchingRecentSearchesPromise = this.recentSearchesService
+ .fetch()
+ .catch(error => {
if (error.name === 'RecentSearchesServiceError') return undefined;
// eslint-disable-next-line no-new
new Flash('An error occurred while parsing recent searches');
// Gracefully fail to empty array
return [];
})
- .then((searches) => {
+ .then(searches => {
if (!searches) {
return;
}
@@ -120,7 +121,7 @@ export default class FilteredSearchManager {
if (this.stateFilters) {
this.searchStateWrapper = this.searchState.bind(this);
- this.applyToStateFilters((filterEl) => {
+ this.applyToStateFilters(filterEl => {
filterEl.addEventListener('click', this.searchStateWrapper);
});
}
@@ -128,14 +129,14 @@ export default class FilteredSearchManager {
unbindStateEvents() {
if (this.stateFilters) {
- this.applyToStateFilters((filterEl) => {
+ this.applyToStateFilters(filterEl => {
filterEl.removeEventListener('click', this.searchStateWrapper);
});
}
}
applyToStateFilters(callback) {
- this.stateFilters.querySelectorAll('a[data-state]').forEach((filterEl) => {
+ this.stateFilters.querySelectorAll('a[data-state]').forEach(filterEl => {
if (this.states.indexOf(filterEl.dataset.state) > -1) {
callback(filterEl);
}
@@ -207,7 +208,7 @@ export default class FilteredSearchManager {
let backspaceCount = 0;
// closure for keeping track of the number of backspace keystrokes
- return (e) => {
+ return e => {
// 8 = Backspace Key
// 46 = Delete Key
if (e.keyCode === 8 || e.keyCode === 46) {
@@ -274,8 +275,12 @@ export default class FilteredSearchManager {
const isElementInDynamicFilterDropdown = e.target.closest('.filter-dropdown') !== null;
const isElementInStaticFilterDropdown = e.target.closest('ul[data-dropdown]') !== null;
- if (!isElementInFilteredSearch && !isElementInDynamicFilterDropdown &&
- !isElementInStaticFilterDropdown && inputContainer) {
+ if (
+ !isElementInFilteredSearch &&
+ !isElementInDynamicFilterDropdown &&
+ !isElementInStaticFilterDropdown &&
+ inputContainer
+ ) {
inputContainer.classList.remove('focus');
}
}
@@ -368,7 +373,7 @@ export default class FilteredSearchManager {
const removeElements = [];
- [].forEach.call(this.tokensContainer.children, (t) => {
+ [].forEach.call(this.tokensContainer.children, t => {
let canClearToken = t.classList.contains('js-visual-token');
if (canClearToken) {
@@ -381,7 +386,7 @@ export default class FilteredSearchManager {
}
});
- removeElements.forEach((el) => {
+ removeElements.forEach(el => {
el.parentElement.removeChild(el);
});
@@ -397,13 +402,14 @@ export default class FilteredSearchManager {
handleInputVisualToken() {
const input = this.filteredSearchInput;
- const { tokens, searchToken }
- = this.tokenizer.processTokens(input.value, this.filteredSearchTokenKeys.getKeys());
- const { isLastVisualTokenValid }
- = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+ const { tokens, searchToken } = this.tokenizer.processTokens(
+ input.value,
+ this.filteredSearchTokenKeys.getKeys(),
+ );
+ const { isLastVisualTokenValid } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
if (isLastVisualTokenValid) {
- tokens.forEach((t) => {
+ tokens.forEach(t => {
input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, '');
FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`, {
uppercaseTokenName: this.filteredSearchTokenKeys.shouldUppercaseTokenName(t.key),
@@ -453,15 +459,17 @@ export default class FilteredSearchManager {
saveCurrentSearchQuery() {
// Don't save before we have fetched the already saved searches
- this.fetchingRecentSearchesPromise.then(() => {
- const searchQuery = DropdownUtils.getSearchQuery();
- if (searchQuery.length > 0) {
- const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery);
- this.recentSearchesService.save(resultantSearches);
- }
- }).catch(() => {
- // https://gitlab.com/gitlab-org/gitlab-ce/issues/30821
- });
+ this.fetchingRecentSearchesPromise
+ .then(() => {
+ const searchQuery = DropdownUtils.getSearchQuery();
+ if (searchQuery.length > 0) {
+ const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery);
+ this.recentSearchesService.save(resultantSearches);
+ }
+ })
+ .catch(() => {
+ // https://gitlab.com/gitlab-org/gitlab-ce/issues/30821
+ });
}
// allows for modifying params array when a param can't be included in the URL (e.g. Service Desk)
@@ -475,7 +483,7 @@ export default class FilteredSearchManager {
const usernameParams = this.getUsernameParams();
let hasFilteredSearch = false;
- params.forEach((p) => {
+ params.forEach(p => {
const split = p.split('=');
const keyParam = decodeURIComponent(split[0]);
const value = split[1];
@@ -486,11 +494,9 @@ export default class FilteredSearchManager {
if (condition) {
hasFilteredSearch = true;
const canEdit = this.canEdit && this.canEdit(condition.tokenKey);
- FilteredSearchVisualTokens.addFilterVisualToken(
- condition.tokenKey,
- condition.value,
- { canEdit },
- );
+ FilteredSearchVisualTokens.addFilterVisualToken(condition.tokenKey, condition.value, {
+ canEdit,
+ });
} else {
// Sanitize value since URL converts spaces into +
// Replace before decode so that we know what was originally + versus the encoded +
@@ -510,7 +516,7 @@ export default class FilteredSearchManager {
if (sanitizedValue.indexOf(' ') !== -1) {
// Prefer ", but use ' if required
- quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\'';
+ quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : "'";
}
hasFilteredSearch = true;
@@ -531,7 +537,9 @@ export default class FilteredSearchManager {
hasFilteredSearch = true;
const tokenName = 'assignee';
const canEdit = this.canEdit && this.canEdit(tokenName);
- FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, { canEdit });
+ FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, {
+ canEdit,
+ });
}
} else if (!match && keyParam === 'author_id') {
const id = parseInt(value, 10);
@@ -539,7 +547,9 @@ export default class FilteredSearchManager {
hasFilteredSearch = true;
const tokenName = 'author';
const canEdit = this.canEdit && this.canEdit(tokenName);
- FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, { canEdit });
+ FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, {
+ canEdit,
+ });
}
} else if (!match && keyParam === 'search') {
hasFilteredSearch = true;
@@ -580,9 +590,11 @@ export default class FilteredSearchManager {
const currentState = state || getParameterByName('state') || 'opened';
paths.push(`state=${currentState}`);
- tokens.forEach((token) => {
- const condition = this.filteredSearchTokenKeys
- .searchByConditionKeyValue(token.key, token.value.toLowerCase());
+ tokens.forEach(token => {
+ const condition = this.filteredSearchTokenKeys.searchByConditionKeyValue(
+ token.key,
+ token.value.toLowerCase(),
+ );
const tokenConfig = this.filteredSearchTokenKeys.searchByKey(token.key) || {};
const { param } = tokenConfig;
@@ -601,8 +613,10 @@ export default class FilteredSearchManager {
tokenValue = tokenValue.toLowerCase();
}
- if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') ||
- (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) {
+ if (
+ (tokenValue[0] === "'" && tokenValue[tokenValue.length - 1] === "'") ||
+ (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')
+ ) {
tokenValue = tokenValue.slice(1, tokenValue.length - 1);
}
@@ -613,7 +627,10 @@ export default class FilteredSearchManager {
});
if (searchToken) {
- const sanitized = searchToken.split(' ').map(t => encodeURIComponent(t)).join('+');
+ const sanitized = searchToken
+ .split(' ')
+ .map(t => encodeURIComponent(t))
+ .join('+');
paths.push(`search=${sanitized}`);
}
@@ -630,7 +647,7 @@ export default class FilteredSearchManager {
const usernamesById = {};
try {
const attribute = this.filteredSearchInput.getAttribute('data-username-params');
- JSON.parse(attribute).forEach((user) => {
+ JSON.parse(attribute).forEach(user => {
usernamesById[user.id] = user.username;
});
} catch (e) {
diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
index c23d4c484a5..89dcff74d0e 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
@@ -135,10 +135,6 @@ export default class FilteredSearchVisualTokens {
}
static updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) {
- if (tokenValue === 'none') {
- return Promise.resolve();
- }
-
const username = tokenValue.replace(/^@/, '');
return (
UsersCache.retrieve(username)
@@ -184,7 +180,12 @@ export default class FilteredSearchVisualTokens {
const tokenValueElement = tokenValueContainer.querySelector('.value');
tokenValueElement.innerText = tokenValue;
+ if (tokenValue === 'none' || tokenValue === 'any') {
+ return;
+ }
+
const tokenType = tokenName.toLowerCase();
+
if (tokenType === 'label') {
FilteredSearchVisualTokens.updateLabelTokenColor(tokenValueContainer, tokenValue);
} else if (tokenType === 'author' || tokenType === 'assignee') {
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 e22f542b7bf..bb0ecb8efe7 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
@@ -68,12 +68,12 @@ export const conditions = [
value: 'any',
},
{
- url: 'milestone_title=No+Milestone',
+ url: 'milestone_title=None',
tokenKey: 'milestone',
value: 'none',
},
{
- url: 'milestone_title=Any+Milestone',
+ url: 'milestone_title=Any',
tokenKey: 'milestone',
value: 'any',
},
@@ -92,6 +92,16 @@ export const conditions = [
tokenKey: 'label',
value: 'none',
},
+ {
+ url: 'my_reaction_emoji=None',
+ tokenKey: 'my-reaction',
+ value: 'none',
+ },
+ {
+ url: 'my_reaction_emoji=Any',
+ tokenKey: 'my-reaction',
+ value: 'any',
+ },
];
const IssuableFilteredSearchTokenKeys = new FilteredSearchTokenKeys(
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index 749c09f897c..c2397842125 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -40,7 +40,9 @@ const createFlashEl = (message, type, isFixedLayout = false) => `
class="flash-${type}"
>
<div
- class="flash-text ${isFixedLayout ? 'container-fluid container-limited limit-container-width' : ''}"
+ class="flash-text ${
+ isFixedLayout ? 'container-fluid container-limited limit-container-width' : ''
+ }"
>
${_.escape(message)}
</div>
@@ -78,7 +80,9 @@ const createFlash = function createFlash(
if (!flashContainer) return null;
- const isFixedLayout = navigation ? navigation.parentNode.classList.contains('container-limited') : true;
+ const isFixedLayout = navigation
+ ? navigation.parentNode.classList.contains('container-limited')
+ : true;
flashContainer.innerHTML = createFlashEl(message, type, isFixedLayout);
diff --git a/app/assets/javascripts/frequent_items/components/app.vue b/app/assets/javascripts/frequent_items/components/app.vue
index 70a8838b772..159c0bdc992 100644
--- a/app/assets/javascripts/frequent_items/components/app.vue
+++ b/app/assets/javascripts/frequent_items/components/app.vue
@@ -1,6 +1,7 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import AccessorUtilities from '~/lib/utils/accessor';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import eventHub from '../event_hub';
import store from '../store/';
import { FREQUENT_ITEMS, STORAGE_KEY } from '../constants';
@@ -14,6 +15,7 @@ export default {
components: {
FrequentItemsSearchInput,
FrequentItemsList,
+ GlLoadingIcon,
},
mixins: [frequentItemsMixin],
props: {
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 7dd0efd622d..00b3d283570 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -94,7 +94,7 @@ class GfmAutoComplete {
...this.getDefaultCallbacks(),
beforeSave(commands) {
if (GfmAutoComplete.isLoading(commands)) return commands;
- return $.map(commands, (c) => {
+ return $.map(commands, c => {
let search = c.name;
if (c.aliases.length > 0) {
search = `${search} ${c.aliases.join(' ')}`;
@@ -167,7 +167,7 @@ class GfmAutoComplete {
callbacks: {
...this.getDefaultCallbacks(),
beforeSave(members) {
- return $.map(members, (m) => {
+ return $.map(members, m => {
let title = '';
if (m.username == null) {
return m;
@@ -178,7 +178,9 @@ class GfmAutoComplete {
}
const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase();
- const imgAvatar = `<img src="${m.avatar_url}" alt="${m.username}" class="avatar avatar-inline center s26"/>`;
+ const imgAvatar = `<img src="${m.avatar_url}" alt="${
+ m.username
+ }" class="avatar avatar-inline center s26"/>`;
const txtAvatar = `<div class="avatar center avatar-inline s26">${autoCompleteAvatar}</div>`;
return {
@@ -211,7 +213,7 @@ class GfmAutoComplete {
callbacks: {
...this.getDefaultCallbacks(),
beforeSave(issues) {
- return $.map(issues, (i) => {
+ return $.map(issues, i => {
if (i.title == null) {
return i;
}
@@ -244,7 +246,7 @@ class GfmAutoComplete {
callbacks: {
...this.getDefaultCallbacks(),
beforeSave(milestones) {
- return $.map(milestones, (m) => {
+ return $.map(milestones, m => {
if (m.title == null) {
return m;
}
@@ -277,7 +279,7 @@ class GfmAutoComplete {
callbacks: {
...this.getDefaultCallbacks(),
beforeSave(merges) {
- return $.map(merges, (m) => {
+ return $.map(merges, m => {
if (m.title == null) {
return m;
}
@@ -324,13 +326,20 @@ class GfmAutoComplete {
},
matcher(flag, subtext) {
const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
- const subtextNodes = subtext.split(/\n+/g).pop().split(GfmAutoComplete.regexSubtext);
+ const subtextNodes = subtext
+ .split(/\n+/g)
+ .pop()
+ .split(GfmAutoComplete.regexSubtext);
// Check if ~ is followed by '/label', '/relabel' or '/unlabel' commands.
- command = subtextNodes.find((node) => {
- if (node === LABEL_COMMAND.LABEL ||
- node === LABEL_COMMAND.RELABEL ||
- node === LABEL_COMMAND.UNLABEL) { return node; }
+ command = subtextNodes.find(node => {
+ if (
+ node === LABEL_COMMAND.LABEL ||
+ node === LABEL_COMMAND.RELABEL ||
+ node === LABEL_COMMAND.UNLABEL
+ ) {
+ return node;
+ }
return null;
});
@@ -380,7 +389,7 @@ class GfmAutoComplete {
callbacks: {
...this.getDefaultCallbacks(),
beforeSave(snippets) {
- return $.map(snippets, (m) => {
+ return $.map(snippets, m => {
if (m.title == null) {
return m;
}
@@ -458,13 +467,17 @@ class GfmAutoComplete {
this.loadData($input, at, validEmojiNames);
GfmAutoComplete.glEmojiTag = glEmojiTag;
})
- .catch(() => { this.isLoadingData[at] = false; });
+ .catch(() => {
+ this.isLoadingData[at] = false;
+ });
} else if (dataSource) {
AjaxCache.retrieve(dataSource, true)
- .then((data) => {
+ .then(data => {
this.loadData($input, at, data);
})
- .catch(() => { this.isLoadingData[at] = false; });
+ .catch(() => {
+ this.isLoadingData[at] = false;
+ });
} else {
this.isLoadingData[at] = false;
}
@@ -497,15 +510,16 @@ class GfmAutoComplete {
}
const loadingState = GfmAutoComplete.defaultLoadingData[0];
- return dataToInspect &&
- (dataToInspect === loadingState || dataToInspect.name === loadingState);
+ return dataToInspect && (dataToInspect === loadingState || dataToInspect.name === loadingState);
}
static defaultMatcher(flag, subtext, controllers) {
// The below is taken from At.js source
// Tweaked to commands to start without a space only if char before is a non-word character
// https://github.com/ichord/At.js
- const atSymbolsWithBar = Object.keys(controllers).join('|').replace(/[$]/, '\\$&');
+ const atSymbolsWithBar = Object.keys(controllers)
+ .join('|')
+ .replace(/[$]/, '\\$&');
const atSymbolsWithoutBar = Object.keys(controllers).join('');
const targetSubtext = subtext.split(GfmAutoComplete.regexSubtext).pop();
const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
@@ -513,7 +527,10 @@ class GfmAutoComplete {
const accentAChar = decodeURI('%C3%80');
const accentYChar = decodeURI('%C3%BF');
- const regexp = new RegExp(`^(?:\\B|[^a-zA-Z0-9_\`${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, 'gi');
+ const regexp = new RegExp(
+ `^(?:\\B|[^a-zA-Z0-9_\`${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`,
+ 'gi',
+ );
return regexp.exec(targetSubtext);
}
@@ -552,8 +569,9 @@ GfmAutoComplete.Members = {
template: '<li>${avatarTag} ${username} <small>${title}</small></li>',
};
GfmAutoComplete.Labels = {
- // eslint-disable-next-line no-template-curly-in-string
- template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>',
+ template:
+ // eslint-disable-next-line no-template-curly-in-string
+ '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>',
};
// Issues, MergeRequests and Snippets
GfmAutoComplete.Issues = {
@@ -567,7 +585,8 @@ GfmAutoComplete.Milestones = {
template: '<li>${title}</li>',
};
GfmAutoComplete.Loading = {
- template: '<li style="pointer-events: none;"><i class="fa fa-spinner fa-spin"></i> Loading...</li>',
+ template:
+ '<li style="pointer-events: none;"><i class="fa fa-spinner fa-spin"></i> Loading...</li>',
};
export default GfmAutoComplete;
diff --git a/app/assets/javascripts/gl_field_errors.js b/app/assets/javascripts/gl_field_errors.js
index 3764e7ab422..d5d5954ce6a 100644
--- a/app/assets/javascripts/gl_field_errors.js
+++ b/app/assets/javascripts/gl_field_errors.js
@@ -28,7 +28,7 @@ export default class GlFieldErrors {
this.form.on('submit', GlFieldErrors.catchInvalidFormSubmit);
}
- /* Neccessary to prevent intercept and override invalid form submit
+ /* Necessary to prevent intercept and override invalid form submit
* because Safari & iOS quietly allow form submission when form is invalid
* and prevents disabling of invalid submit button by application.js */
diff --git a/app/assets/javascripts/group.js b/app/assets/javascripts/group.js
index 4365305c168..903c838e266 100644
--- a/app/assets/javascripts/group.js
+++ b/app/assets/javascripts/group.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { slugifyWithHyphens } from './lib/utils/text_utility';
export default class Group {
constructor() {
@@ -7,17 +8,18 @@ export default class Group {
this.updateHandler = this.update.bind(this);
this.resetHandler = this.reset.bind(this);
if (this.groupName.val() === '') {
- this.groupPath.on('keyup', this.updateHandler);
- this.groupName.on('keydown', this.resetHandler);
+ this.groupName.on('keyup', this.updateHandler);
+ this.groupPath.on('keydown', this.resetHandler);
}
}
update() {
- this.groupName.val(this.groupPath.val());
+ const slug = slugifyWithHyphens(this.groupName.val());
+ this.groupPath.val(slug);
}
reset() {
- this.groupPath.off('keyup', this.updateHandler);
- this.groupName.off('keydown', this.resetHandler);
+ this.groupName.off('keyup', this.updateHandler);
+ this.groupPath.off('keydown', this.resetHandler);
}
}
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
index a032f291546..2a4a39436e7 100644
--- a/app/assets/javascripts/groups/components/app.vue
+++ b/app/assets/javascripts/groups/components/app.vue
@@ -8,6 +8,7 @@ import { HIDDEN_CLASS } from '~/lib/utils/constants';
import { getParameterByName } from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import eventHub from '../event_hub';
import { COMMON_STR, CONTENT_LIST_CLASS } from '../constants';
import groupsComponent from './groups.vue';
@@ -16,6 +17,7 @@ export default {
components: {
DeprecatedModal,
groupsComponent,
+ GlLoadingIcon,
},
props: {
action: {
diff --git a/app/assets/javascripts/ide/components/branches/search_list.vue b/app/assets/javascripts/ide/components/branches/search_list.vue
index 52ccc537c9d..358f1153de2 100644
--- a/app/assets/javascripts/ide/components/branches/search_list.vue
+++ b/app/assets/javascripts/ide/components/branches/search_list.vue
@@ -2,12 +2,14 @@
import { mapActions, mapState } from 'vuex';
import _ from 'underscore';
import Icon from '~/vue_shared/components/icon.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import Item from './item.vue';
export default {
components: {
Item,
Icon,
+ GlLoadingIcon,
},
data() {
return {
diff --git a/app/assets/javascripts/ide/components/error_message.vue b/app/assets/javascripts/ide/components/error_message.vue
index a20dc0a7006..2d9bd99e82a 100644
--- a/app/assets/javascripts/ide/components/error_message.vue
+++ b/app/assets/javascripts/ide/components/error_message.vue
@@ -1,7 +1,11 @@
<script>
import { mapActions } from 'vuex';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
+ components: {
+ GlLoadingIcon,
+ },
props: {
message: {
type: Object,
diff --git a/app/assets/javascripts/ide/components/file_templates/dropdown.vue b/app/assets/javascripts/ide/components/file_templates/dropdown.vue
index 94222c08e91..891f7d48b4c 100644
--- a/app/assets/javascripts/ide/components/file_templates/dropdown.vue
+++ b/app/assets/javascripts/ide/components/file_templates/dropdown.vue
@@ -2,10 +2,12 @@
import $ from 'jquery';
import { mapActions, mapState } from 'vuex';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
components: {
DropdownButton,
+ GlLoadingIcon,
},
props: {
data: {
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index ad6151e3bf6..0a368f6558c 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -43,7 +43,7 @@ export default {
'currentProjectId',
'errorMessage',
]),
- ...mapGetters(['activeFile', 'hasChanges', 'someUncommitedChanges', 'isCommitModeActive']),
+ ...mapGetters(['activeFile', 'hasChanges', 'someUncommittedChanges', 'isCommitModeActive']),
},
mounted() {
window.onbeforeunload = e => this.onBeforeUnload(e);
@@ -63,7 +63,7 @@ export default {
onBeforeUnload(e = {}) {
const returnValue = __('Are you sure you want to lose unsaved changes?');
- if (!this.someUncommitedChanges) return undefined;
+ if (!this.someUncommittedChanges) return undefined;
Object.assign(e, {
returnValue,
diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue
index d4c430cd2f3..364ab9426e0 100644
--- a/app/assets/javascripts/ide/components/ide_side_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_side_bar.vue
@@ -25,11 +25,11 @@ export default {
},
computed: {
...mapState(['loading', 'currentActivityView', 'changedFiles', 'stagedFiles', 'lastCommitMsg']),
- ...mapGetters(['currentProject', 'someUncommitedChanges']),
+ ...mapGetters(['currentProject', 'someUncommittedChanges']),
showSuccessMessage() {
return (
this.currentActivityView === activityBarViews.edit &&
- (this.lastCommitMsg && !this.someUncommitedChanges)
+ (this.lastCommitMsg && !this.someUncommittedChanges)
);
},
},
diff --git a/app/assets/javascripts/ide/components/jobs/list.vue b/app/assets/javascripts/ide/components/jobs/list.vue
index acd37605d16..57da8b4e2cb 100644
--- a/app/assets/javascripts/ide/components/jobs/list.vue
+++ b/app/assets/javascripts/ide/components/jobs/list.vue
@@ -1,10 +1,12 @@
<script>
import { mapActions } from 'vuex';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import Stage from './stage.vue';
export default {
components: {
Stage,
+ GlLoadingIcon,
},
props: {
stages: {
diff --git a/app/assets/javascripts/ide/components/jobs/stage.vue b/app/assets/javascripts/ide/components/jobs/stage.vue
index ec168d36b9e..5644759d2f9 100644
--- a/app/assets/javascripts/ide/components/jobs/stage.vue
+++ b/app/assets/javascripts/ide/components/jobs/stage.vue
@@ -1,4 +1,5 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import tooltip from '../../../vue_shared/directives/tooltip';
import Icon from '../../../vue_shared/components/icon.vue';
import CiIcon from '../../../vue_shared/components/ci_icon.vue';
@@ -12,6 +13,7 @@ export default {
Icon,
CiIcon,
Item,
+ GlLoadingIcon,
},
props: {
stage: {
diff --git a/app/assets/javascripts/ide/components/merge_requests/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue
index f5e42e87f1b..e4000f588bd 100644
--- a/app/assets/javascripts/ide/components/merge_requests/list.vue
+++ b/app/assets/javascripts/ide/components/merge_requests/list.vue
@@ -3,6 +3,7 @@ import { mapActions, mapState } from 'vuex';
import _ from 'underscore';
import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import Item from './item.vue';
import TokenedInput from '../shared/tokened_input.vue';
@@ -16,6 +17,7 @@ export default {
TokenedInput,
Item,
Icon,
+ GlLoadingIcon,
},
data() {
return {
diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue
index b670b0355b7..16aec1decd6 100644
--- a/app/assets/javascripts/ide/components/pipelines/list.vue
+++ b/app/assets/javascripts/ide/components/pipelines/list.vue
@@ -1,6 +1,7 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import { sprintf, __ } from '../../../locale';
import Icon from '../../../vue_shared/components/icon.vue';
import CiIcon from '../../../vue_shared/components/ci_icon.vue';
@@ -17,6 +18,7 @@ export default {
Tab,
JobsList,
EmptyState,
+ GlLoadingIcon,
},
computed: {
...mapState(['pipelinesEmptyStateSvgPath', 'links']),
diff --git a/app/assets/javascripts/ide/components/preview/clientside.vue b/app/assets/javascripts/ide/components/preview/clientside.vue
index 37a8ad36507..0bd56ff6e9b 100644
--- a/app/assets/javascripts/ide/components/preview/clientside.vue
+++ b/app/assets/javascripts/ide/components/preview/clientside.vue
@@ -3,6 +3,7 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore';
import { Manager } from 'smooshpack';
import { listen } from 'codesandbox-api';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import Navigator from './navigator.vue';
import { packageJsonPath } from '../../constants';
import { createPathWithExt } from '../../utils';
@@ -10,6 +11,7 @@ import { createPathWithExt } from '../../utils';
export default {
components: {
Navigator,
+ GlLoadingIcon,
},
data() {
return {
diff --git a/app/assets/javascripts/ide/components/preview/navigator.vue b/app/assets/javascripts/ide/components/preview/navigator.vue
index 42f23801692..af8959186f9 100644
--- a/app/assets/javascripts/ide/components/preview/navigator.vue
+++ b/app/assets/javascripts/ide/components/preview/navigator.vue
@@ -1,10 +1,12 @@
<script>
import { listen } from 'codesandbox-api';
import Icon from '~/vue_shared/components/icon.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
components: {
Icon,
+ GlLoadingIcon,
},
props: {
manager: {
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index d3b24c5b793..5e86876c1c1 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -27,10 +27,10 @@ export default {
'unusedSeal',
]),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
- ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges', 'activeFile']),
+ ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommittedChanges', 'activeFile']),
...mapGetters('commit', ['discardDraftButtonDisabled']),
showStageUnstageArea() {
- return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal);
+ return !!(this.someUncommittedChanges || this.lastCommitMsg || !this.unusedSeal);
},
activeFileKey() {
return this.activeFile ? this.activeFile.key : null;
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index 709748fb530..8ad85074d6b 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -63,7 +63,7 @@ export const isEditModeActive = state => state.currentActivityView === activityB
export const isCommitModeActive = state => state.currentActivityView === activityBarViews.commit;
export const isReviewModeActive = state => state.currentActivityView === activityBarViews.review;
-export const someUncommitedChanges = state =>
+export const someUncommittedChanges = state =>
!!(state.changedFiles.length || state.stagedFiles.length);
export const getChangesInFolder = state => path => {
diff --git a/app/assets/javascripts/jobs/components/artifacts_block.vue b/app/assets/javascripts/jobs/components/artifacts_block.vue
index 17fd5321642..93c89411b4a 100644
--- a/app/assets/javascripts/jobs/components/artifacts_block.vue
+++ b/app/assets/javascripts/jobs/components/artifacts_block.vue
@@ -1,10 +1,12 @@
<script>
+import { GlLink } from '@gitlab-org/gitlab-ui';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
export default {
components: {
TimeagoTooltip,
+ GlLink,
},
mixins: [timeagoMixin],
props: {
@@ -53,16 +55,16 @@ export default {
class="btn-group d-flex"
role="group"
>
- <a
+ <gl-link
v-if="artifact.keep_path"
:href="artifact.keep_path"
class="js-keep-artifacts btn btn-sm btn-default"
data-method="post"
>
{{ s__('Job|Keep') }}
- </a>
+ </gl-link>
- <a
+ <gl-link
v-if="artifact.download_path"
:href="artifact.download_path"
class="js-download-artifacts btn btn-sm btn-default"
@@ -70,15 +72,15 @@ export default {
rel="nofollow"
>
{{ s__('Job|Download') }}
- </a>
+ </gl-link>
- <a
+ <gl-link
v-if="artifact.browse_path"
:href="artifact.browse_path"
class="js-browse-artifacts btn btn-sm btn-default"
>
{{ s__('Job|Browse') }}
- </a>
+ </gl-link>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/commit_block.vue b/app/assets/javascripts/jobs/components/commit_block.vue
index 7d51f6afd10..06fe23fedce 100644
--- a/app/assets/javascripts/jobs/components/commit_block.vue
+++ b/app/assets/javascripts/jobs/components/commit_block.vue
@@ -1,9 +1,11 @@
<script>
+import { GlLink } from '@gitlab-org/gitlab-ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default {
components: {
ClipboardButton,
+ GlLink,
},
props: {
commit: {
@@ -31,10 +33,10 @@ export default {
<p>
{{ __('Commit') }}
- <a
+ <gl-link
:href="commit.commit_path"
class="js-commit-sha commit-sha link-commit"
- >{{ commit.short_id }}</a>
+ >{{ commit.short_id }}</gl-link>
<clipboard-button
:text="commit.short_id"
@@ -42,11 +44,11 @@ export default {
css-class="btn btn-clipboard btn-transparent"
/>
- <a
+ <gl-link
v-if="mergeRequest"
:href="mergeRequest.path"
class="js-link-commit link-commit"
- >!{{ mergeRequest.iid }}</a>
+ >!{{ mergeRequest.iid }}</gl-link>
</p>
<p class="build-light-text append-bottom-0">
diff --git a/app/assets/javascripts/jobs/components/empty_state.vue b/app/assets/javascripts/jobs/components/empty_state.vue
index ee5ceb99b0a..be7425c2d25 100644
--- a/app/assets/javascripts/jobs/components/empty_state.vue
+++ b/app/assets/javascripts/jobs/components/empty_state.vue
@@ -1,5 +1,10 @@
<script>
+import { GlLink } from '@gitlab-org/gitlab-ui';
+
export default {
+ components: {
+ GlLink,
+ },
props: {
illustrationPath: {
type: String,
@@ -62,13 +67,13 @@ export default {
v-if="action"
class="text-center"
>
- <a
+ <gl-link
:href="action.path"
:data-method="action.method"
class="js-job-empty-state-action btn btn-primary"
>
{{ action.button_title }}
- </a>
+ </gl-link>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/jobs/components/erased_block.vue b/app/assets/javascripts/jobs/components/erased_block.vue
index 5ffbfb6e19a..d80e905c68e 100644
--- a/app/assets/javascripts/jobs/components/erased_block.vue
+++ b/app/assets/javascripts/jobs/components/erased_block.vue
@@ -1,10 +1,12 @@
<script>
import _ from 'underscore';
+import { GlLink } from '@gitlab-org/gitlab-ui';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
TimeagoTooltip,
+ GlLink,
},
props: {
user: {
@@ -29,9 +31,9 @@ export default {
<div class="erased alert alert-warning">
<template v-if="isErasedByUser">
{{ s__("Job|Job has been erased by") }}
- <a :href="user.web_url">
+ <gl-link :href="user.web_url">
{{ user.username }}
- </a>
+ </gl-link>
</template>
<template v-else>
{{ s__("Job|Job has been erased") }}
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index ac19034f69d..90216b04e92 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -1,164 +1,188 @@
<script>
- import _ from 'underscore';
- import { mapGetters, mapState, mapActions } from 'vuex';
- import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
- import bp from '~/breakpoints';
- import CiHeader from '~/vue_shared/components/header_ci_component.vue';
- import Callout from '~/vue_shared/components/callout.vue';
- import createStore from '../store';
- import EmptyState from './empty_state.vue';
- import EnvironmentsBlock from './environments_block.vue';
- import ErasedBlock from './erased_block.vue';
- import Log from './job_log.vue';
- import LogTopBar from './job_log_controllers.vue';
- import StuckBlock from './stuck_block.vue';
- import Sidebar from './sidebar.vue';
+import _ from 'underscore';
+import { mapGetters, mapState, mapActions } from 'vuex';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
+import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
+import { polyfillSticky } from '~/lib/utils/sticky';
+import bp from '~/breakpoints';
+import CiHeader from '~/vue_shared/components/header_ci_component.vue';
+import Callout from '~/vue_shared/components/callout.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import createStore from '../store';
+import EmptyState from './empty_state.vue';
+import EnvironmentsBlock from './environments_block.vue';
+import ErasedBlock from './erased_block.vue';
+import Log from './job_log.vue';
+import LogTopBar from './job_log_controllers.vue';
+import StuckBlock from './stuck_block.vue';
+import Sidebar from './sidebar.vue';
+import { sprintf } from '~/locale';
+import delayedJobMixin from '../mixins/delayed_job_mixin';
- export default {
- name: 'JobPageApp',
- store: createStore(),
- components: {
- CiHeader,
- Callout,
- EmptyState,
- EnvironmentsBlock,
- ErasedBlock,
- Log,
- LogTopBar,
- StuckBlock,
- Sidebar,
+export default {
+ name: 'JobPageApp',
+ store: createStore(),
+ components: {
+ CiHeader,
+ Callout,
+ EmptyState,
+ EnvironmentsBlock,
+ ErasedBlock,
+ Icon,
+ Log,
+ LogTopBar,
+ StuckBlock,
+ Sidebar,
+ GlLoadingIcon,
+ },
+ mixins: [delayedJobMixin],
+ props: {
+ runnerSettingsUrl: {
+ type: String,
+ required: false,
+ default: null,
},
- props: {
- runnerSettingsUrl: {
- type: String,
- required: false,
- default: null,
- },
- runnerHelpUrl: {
- type: String,
- required: false,
- default: null,
- },
- endpoint: {
- type: String,
- required: true,
- },
- terminalPath: {
- type: String,
- required: false,
- default: null,
- },
- pagePath: {
- type: String,
- required: true,
- },
- logState: {
- type: String,
- required: true,
- },
+ runnerHelpUrl: {
+ type: String,
+ required: false,
+ default: null,
},
- computed: {
- ...mapState([
- 'isLoading',
- 'job',
- 'isSidebarOpen',
- 'trace',
- 'isTraceComplete',
- 'traceSize',
- 'isTraceSizeVisible',
- 'isScrollBottomDisabled',
- 'isScrollTopDisabled',
- 'isScrolledToBottomBeforeReceivingTrace',
- 'hasError',
- ]),
- ...mapGetters([
- 'headerActions',
- 'headerTime',
- 'shouldRenderCalloutMessage',
- 'shouldRenderTriggeredLabel',
- 'hasEnvironment',
- 'hasTrace',
- 'emptyStateIllustration',
- 'isScrollingDown',
- 'emptyStateAction',
- 'hasRunnersForProject',
- ]),
-
- shouldRenderContent() {
- return !this.isLoading && !this.hasError;
- }
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ terminalPath: {
+ type: String,
+ required: false,
+ default: null,
},
- watch: {
- // Once the job log is loaded,
- // fetch the stages for the dropdown on the sidebar
- job(newVal, oldVal) {
- if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) {
- this.fetchStages();
- }
- },
+ pagePath: {
+ type: String,
+ required: true,
},
- created() {
- this.throttled = _.throttle(this.toggleScrollButtons, 100);
+ logState: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState([
+ 'isLoading',
+ 'job',
+ 'isSidebarOpen',
+ 'trace',
+ 'isTraceComplete',
+ 'traceSize',
+ 'isTraceSizeVisible',
+ 'isScrollBottomDisabled',
+ 'isScrollTopDisabled',
+ 'isScrolledToBottomBeforeReceivingTrace',
+ 'hasError',
+ ]),
+ ...mapGetters([
+ 'headerActions',
+ 'headerTime',
+ 'shouldRenderCalloutMessage',
+ 'shouldRenderTriggeredLabel',
+ 'hasEnvironment',
+ 'hasTrace',
+ 'emptyStateIllustration',
+ 'isScrollingDown',
+ 'emptyStateAction',
+ 'hasRunnersForProject',
+ ]),
- this.setJobEndpoint(this.endpoint);
- this.setTraceOptions({
- logState: this.logState,
- pagePath: this.pagePath,
- });
+ shouldRenderContent() {
+ return !this.isLoading && !this.hasError;
+ },
- this.fetchJob();
- this.fetchTrace();
+ emptyStateTitle() {
+ const { emptyStateIllustration, remainingTime } = this;
+ const { title } = emptyStateIllustration;
- window.addEventListener('resize', this.onResize);
- window.addEventListener('scroll', this.updateScroll);
- },
+ if (this.isDelayedJob) {
+ return sprintf(title, { remainingTime });
+ }
- mounted() {
- this.updateSidebar();
+ return title;
},
+ },
+ watch: {
+ // Once the job log is loaded,
+ // fetch the stages for the dropdown on the sidebar
+ job(newVal, oldVal) {
+ if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) {
+ this.fetchStages();
+ }
- destroyed() {
- window.removeEventListener('resize', this.onResize);
- window.removeEventListener('scroll', this.updateScroll);
+ if (newVal.archived) {
+ this.$nextTick(() => {
+ if (this.$refs.sticky) {
+ polyfillSticky(this.$refs.sticky);
+ }
+ });
+ }
},
+ },
+ created() {
+ this.throttled = _.throttle(this.toggleScrollButtons, 100);
+
+ this.setJobEndpoint(this.endpoint);
+ this.setTraceOptions({
+ logState: this.logState,
+ pagePath: this.pagePath,
+ });
- methods: {
- ...mapActions([
- 'setJobEndpoint',
- 'setTraceOptions',
- 'fetchJob',
- 'fetchStages',
- 'hideSidebar',
- 'showSidebar',
- 'toggleSidebar',
- 'fetchTrace',
- 'scrollBottom',
- 'scrollTop',
- 'toggleScrollButtons',
- 'toggleScrollAnimation',
- ]),
- onResize() {
- this.updateSidebar();
- this.updateScroll();
- },
- updateSidebar() {
- if (bp.getBreakpointSize() === 'xs') {
- this.hideSidebar();
- } else if (!this.isSidebarOpen) {
- this.showSidebar();
- }
- },
- updateScroll() {
- if (!isScrolledToBottom()) {
- this.toggleScrollAnimation(false);
- } else if (this.isScrollingDown) {
- this.toggleScrollAnimation(true);
- }
+ this.fetchJob();
+ this.fetchTrace();
- this.throttled();
- },
+ window.addEventListener('resize', this.onResize);
+ window.addEventListener('scroll', this.updateScroll);
+ },
+ mounted() {
+ this.updateSidebar();
+ },
+ destroyed() {
+ window.removeEventListener('resize', this.onResize);
+ window.removeEventListener('scroll', this.updateScroll);
+ },
+ methods: {
+ ...mapActions([
+ 'setJobEndpoint',
+ 'setTraceOptions',
+ 'fetchJob',
+ 'fetchStages',
+ 'hideSidebar',
+ 'showSidebar',
+ 'toggleSidebar',
+ 'fetchTrace',
+ 'scrollBottom',
+ 'scrollTop',
+ 'toggleScrollButtons',
+ 'toggleScrollAnimation',
+ ]),
+ onResize() {
+ this.updateSidebar();
+ this.updateScroll();
+ },
+ updateSidebar() {
+ if (bp.getBreakpointSize() === 'xs') {
+ this.hideSidebar();
+ } else if (!this.isSidebarOpen) {
+ this.showSidebar();
+ }
+ },
+ updateScroll() {
+ if (!isScrolledToBottom()) {
+ this.toggleScrollAnimation(false);
+ } else if (this.isScrollingDown) {
+ this.toggleScrollAnimation(true);
+ }
+
+ this.throttled();
},
- };
+ },
+};
</script>
<template>
<div>
@@ -216,14 +240,28 @@
:erased-at="job.erased_at"
/>
+ <div
+ v-if="job.archived"
+ ref="sticky"
+ class="js-archived-job prepend-top-default archived-sticky sticky-top"
+ >
+ <icon
+ name="lock"
+ class="align-text-bottom"
+ />
+
+ {{ __('This job is archived. Only the complete pipeline can be retried.') }}
+ </div>
<!--job log -->
<div
v-if="hasTrace"
- class="build-trace-container prepend-top-default">
+ class="build-trace-container"
+ >
<log-top-bar
:class="{
'sidebar-expanded': isSidebarOpen,
- 'sidebar-collapsed': !isSidebarOpen
+ 'sidebar-collapsed': !isSidebarOpen,
+ 'has-archived-block': job.archived
}"
:erase-path="job.erase_path"
:size="traceSize"
@@ -248,7 +286,7 @@
class="js-job-empty-state"
:illustration-path="emptyStateIllustration.image"
:illustration-size-class="emptyStateIllustration.size"
- :title="emptyStateIllustration.title"
+ :title="emptyStateTitle"
:content="emptyStateIllustration.content"
:action="emptyStateAction"
/>
diff --git a/app/assets/javascripts/jobs/components/job_container_item.vue b/app/assets/javascripts/jobs/components/job_container_item.vue
index 6486b25c8a7..3ddcfd11dca 100644
--- a/app/assets/javascripts/jobs/components/job_container_item.vue
+++ b/app/assets/javascripts/jobs/components/job_container_item.vue
@@ -1,16 +1,21 @@
<script>
+import { GlLink } from '@gitlab-org/gitlab-ui';
+import tooltip from '~/vue_shared/directives/tooltip';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
-import tooltip from '~/vue_shared/directives/tooltip';
+import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
+import { sprintf } from '~/locale';
export default {
components: {
CiIcon,
Icon,
+ GlLink,
},
directives: {
tooltip,
},
+ mixins: [delayedJobMixin],
props: {
job: {
type: Object,
@@ -23,7 +28,14 @@ export default {
},
computed: {
tooltipText() {
- return `${this.job.name} - ${this.job.status.tooltip}`;
+ const { name, status } = this.job;
+ const text = `${name} - ${status.tooltip}`;
+
+ if (this.isDelayedJob) {
+ return sprintf(text, { remainingTime: this.remainingTime });
+ }
+
+ return text;
},
},
};
@@ -37,11 +49,10 @@ export default {
active: isActive
}"
>
- <a
+ <gl-link
v-tooltip
:href="job.status.details_path"
:title="tooltipText"
- data-container="body"
data-boundary="viewport"
class="js-job-link"
>
@@ -60,6 +71,6 @@ export default {
name="retry"
class="js-retry-icon"
/>
- </a>
+ </gl-link>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/job_log.vue b/app/assets/javascripts/jobs/components/job_log.vue
index ffa6ada3e28..92e20e92d66 100644
--- a/app/assets/javascripts/jobs/components/job_log.vue
+++ b/app/assets/javascripts/jobs/components/job_log.vue
@@ -1,45 +1,45 @@
<script>
- import { mapState, mapActions } from 'vuex';
+import { mapState, mapActions } from 'vuex';
- export default {
- name: 'JobLog',
- props: {
- trace: {
- type: String,
- required: true,
- },
- isComplete: {
- type: Boolean,
- required: true,
- },
+export default {
+ name: 'JobLog',
+ props: {
+ trace: {
+ type: String,
+ required: true,
},
- computed: {
- ...mapState(['isScrolledToBottomBeforeReceivingTrace']),
+ isComplete: {
+ type: Boolean,
+ required: true,
},
- updated() {
- this.$nextTick(() => this.handleScrollDown());
+ },
+ computed: {
+ ...mapState(['isScrolledToBottomBeforeReceivingTrace']),
+ },
+ updated() {
+ this.$nextTick(() => this.handleScrollDown());
+ },
+ mounted() {
+ this.$nextTick(() => this.handleScrollDown());
+ },
+ methods: {
+ ...mapActions(['scrollBottom']),
+ /**
+ * The job log is sent in HTML, which means we need to use `v-html` to render it
+ * Using the updated hook with $nextTick is not enough to wait for the DOM to be updated
+ * in this case because it runs before `v-html` has finished running, since there's no
+ * Vue binding.
+ * In order to scroll the page down after `v-html` has finished, we need to use setTimeout
+ */
+ handleScrollDown() {
+ if (this.isScrolledToBottomBeforeReceivingTrace) {
+ setTimeout(() => {
+ this.scrollBottom();
+ }, 0);
+ }
},
- mounted() {
- this.$nextTick(() => this.handleScrollDown());
- },
- methods: {
- ...mapActions(['scrollBottom']),
- /**
- * The job log is sent in HTML, which means we need to use `v-html` to render it
- * Using the updated hook with $nextTick is not enough to wait for the DOM to be updated
- * in this case because it runs before `v-html` has finished running, since there's no
- * Vue binding.
- * In order to scroll the page down after `v-html` has finished, we need to use setTimeout
- */
- handleScrollDown() {
- if (this.isScrolledToBottomBeforeReceivingTrace) {
- setTimeout(() => {
- this.scrollBottom();
- }, 0);
- }
- },
- },
- };
+ },
+};
</script>
<template>
<pre class="js-build-trace build-trace qa-build-trace">
diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue
index 94ab1b16c84..8b506b124ec 100644
--- a/app/assets/javascripts/jobs/components/job_log_controllers.vue
+++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue
@@ -1,7 +1,7 @@
<script>
+import { GlTooltipDirective, GlLink, GlButton } from '@gitlab-org/gitlab-ui';
import { polyfillSticky } from '~/lib/utils/sticky';
import Icon from '~/vue_shared/components/icon.vue';
-import tooltip from '~/vue_shared/directives/tooltip';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { sprintf } from '~/locale';
import scrollDown from '../svg/scroll_down.svg';
@@ -9,9 +9,11 @@ import scrollDown from '../svg/scroll_down.svg';
export default {
components: {
Icon,
+ GlLink,
+ GlButton,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
scrollDown,
props: {
@@ -67,82 +69,76 @@ export default {
};
</script>
<template>
- <div class="top-bar affix">
+ <div class="top-bar">
<!-- truncate information -->
<div class="js-truncated-info truncated-info d-none d-sm-block float-left">
<template v-if="isTraceSizeVisible">
{{ jobLogSize }}
- <a
+ <gl-link
v-if="rawPath"
:href="rawPath"
class="js-raw-link raw-link"
>
{{ s__("Job|Complete Raw") }}
- </a>
+ </gl-link>
</template>
</div>
<!-- eo truncate information -->
<div class="controllers float-right">
<!-- links -->
- <a
+ <gl-link
v-if="rawPath"
- v-tooltip
+ v-gl-tooltip.body
:title="s__('Job|Show complete raw')"
:href="rawPath"
class="js-raw-link-controller controllers-buttons"
- data-container="body"
>
<icon name="doc-text" />
- </a>
+ </gl-link>
- <a
+ <gl-link
v-if="erasePath"
- v-tooltip
+ v-gl-tooltip.body
:title="s__('Job|Erase job log')"
:href="erasePath"
:data-confirm="__('Are you sure you want to erase this build?')"
class="js-erase-link controllers-buttons"
- data-container="body"
data-method="post"
>
<icon name="remove" />
- </a>
+ </gl-link>
<!-- eo links -->
<!-- scroll buttons -->
<div
- v-tooltip
+ v-gl-tooltip
:title="s__('Job|Scroll to top')"
class="controllers-buttons"
- data-container="body"
>
- <button
+ <gl-button
:disabled="isScrollTopDisabled"
type="button"
class="js-scroll-top btn-scroll btn-transparent btn-blank"
@click="handleScrollToTop"
>
- <icon name="scroll_up"/>
- </button>
+ <icon name="scroll_up" />
+ </gl-button>
</div>
<div
- v-tooltip
+ v-gl-tooltip
:title="s__('Job|Scroll to bottom')"
class="controllers-buttons"
- data-container="body"
>
- <button
+ <gl-button
:disabled="isScrollBottomDisabled"
- type="button"
class="js-scroll-bottom btn-scroll btn-transparent btn-blank"
:class="{ animate: isScrollingDown }"
@click="handleScrollToBottom"
v-html="$options.scrollDown"
- >
- </button>
+ />
</div>
<!-- eo scroll buttons -->
</div>
diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue
index 28a02230d89..f7b7b8f10f7 100644
--- a/app/assets/javascripts/jobs/components/sidebar.vue
+++ b/app/assets/javascripts/jobs/components/sidebar.vue
@@ -1,6 +1,7 @@
<script>
import _ from 'underscore';
import { mapActions, mapState } from 'vuex';
+import { GlLink, GlButton } from '@gitlab-org/gitlab-ui';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import Icon from '~/vue_shared/components/icon.vue';
@@ -21,6 +22,8 @@ export default {
TriggerBlock,
StagesDropdown,
JobsContainer,
+ GlLink,
+ GlButton,
},
mixins: [timeagoMixin],
props: {
@@ -115,7 +118,7 @@ export default {
<strong class="inline prepend-top-8">
{{ job.name }}
</strong>
- <a
+ <gl-link
v-if="job.retry_path"
:class="retryButtonClass"
:href="job.retry_path"
@@ -123,8 +126,8 @@ export default {
rel="nofollow"
>
{{ __('Retry') }}
- </a>
- <a
+ </gl-link>
+ <gl-link
v-if="job.terminal_path"
:href="job.terminal_path"
class="js-terminal-link pull-right btn btn-primary
@@ -133,8 +136,8 @@ export default {
>
{{ __('Debug') }}
<icon name="external-link" />
- </a>
- <button
+ </gl-link>
+ <gl-button
:aria-label="__('Toggle Sidebar')"
type="button"
class="btn btn-blank gutter-toggle
@@ -146,20 +149,20 @@ export default {
data-hidden="true"
class="fa fa-angle-double-right"
></i>
- </button>
+ </gl-button>
</div>
<div
v-if="job.retry_path || job.new_issue_path"
class="block retry-link"
>
- <a
+ <gl-link
v-if="job.new_issue_path"
:href="job.new_issue_path"
class="js-new-issue btn btn-success btn-inverted"
>
{{ __('New issue') }}
- </a>
- <a
+ </gl-link>
+ <gl-link
v-if="job.retry_path"
:href="job.retry_path"
class="js-retry-job btn btn-inverted-secondary"
@@ -167,7 +170,7 @@ export default {
rel="nofollow"
>
{{ __('Retry') }}
- </a>
+ </gl-link>
</div>
<div :class="{ block : renderBlock }">
<p
@@ -177,9 +180,9 @@ export default {
<span class="build-light-text">
{{ __('Merge Request:') }}
</span>
- <a :href="job.merge_request.path">
+ <gl-link :href="job.merge_request.path">
!{{ job.merge_request.iid }}
- </a>
+ </gl-link>
</p>
<detail-row
@@ -244,14 +247,14 @@ export default {
v-if="job.cancel_path"
class="btn-group prepend-top-5"
role="group">
- <a
+ <gl-link
:href="job.cancel_path"
class="js-cancel-job btn btn-sm btn-default"
data-method="post"
rel="nofollow"
>
{{ __('Cancel') }}
- </a>
+ </gl-link>
</div>
</div>
diff --git a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
index aeafe98a70b..cfedb38e17a 100644
--- a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
@@ -1,6 +1,11 @@
<script>
+import { GlLink } from '@gitlab-org/gitlab-ui';
+
export default {
name: 'SidebarDetailRow',
+ components: {
+ GlLink,
+ },
props: {
title: {
type: String,
@@ -41,7 +46,7 @@ export default {
v-if="hasHelpURL"
class="help-button float-right"
>
- <a
+ <gl-link
:href="helpUrl"
target="_blank"
rel="noopener noreferrer nofollow"
@@ -50,7 +55,7 @@ export default {
class="fa fa-question-circle"
aria-hidden="true"
></i>
- </a>
+ </gl-link>
</span>
</p>
</template>
diff --git a/app/assets/javascripts/jobs/components/stuck_block.vue b/app/assets/javascripts/jobs/components/stuck_block.vue
index 1d5789b175a..ca4bf471363 100644
--- a/app/assets/javascripts/jobs/components/stuck_block.vue
+++ b/app/assets/javascripts/jobs/components/stuck_block.vue
@@ -1,8 +1,12 @@
<script>
+import { GlLink } from '@gitlab-org/gitlab-ui';
/**
* Renders Stuck Runners block for job's view.
*/
export default {
+ components: {
+ GlLink,
+ },
props: {
hasNoRunnersForProject: {
type: Boolean,
@@ -52,12 +56,12 @@ export default {
</p>
{{ __("Go to") }}
- <a
+ <gl-link
v-if="runnersPath"
:href="runnersPath"
class="js-runners-path"
>
{{ __("Runners page") }}
- </a>
+ </gl-link>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/trigger_block.vue b/app/assets/javascripts/jobs/components/trigger_block.vue
index 41de4a6e85a..1e62c05b4d1 100644
--- a/app/assets/javascripts/jobs/components/trigger_block.vue
+++ b/app/assets/javascripts/jobs/components/trigger_block.vue
@@ -1,5 +1,10 @@
<script>
+import { GlButton } from '@gitlab-org/gitlab-ui';
+
export default {
+ components: {
+ GlButton,
+ },
props: {
trigger: {
type: Object,
@@ -41,15 +46,14 @@ export default {
</p>
<p v-if="hasVariables">
- <button
+ <gl-button
v-if="!areVariablesVisible"
type="button"
class="btn btn-default group js-reveal-variables"
@click="revealVariables"
>
{{ __('Reveal Variables') }}
- </button>
-
+ </gl-button>
</p>
<dl
diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js
index ccd096a1da5..a32e945627c 100644
--- a/app/assets/javascripts/jobs/index.js
+++ b/app/assets/javascripts/jobs/index.js
@@ -23,4 +23,3 @@ export default () => {
},
});
};
-
diff --git a/app/assets/javascripts/jobs/mixins/delayed_job_mixin.js b/app/assets/javascripts/jobs/mixins/delayed_job_mixin.js
new file mode 100644
index 00000000000..8c7fb785a61
--- /dev/null
+++ b/app/assets/javascripts/jobs/mixins/delayed_job_mixin.js
@@ -0,0 +1,50 @@
+import { calculateRemainingMilliseconds, formatTime } from '~/lib/utils/datetime_utility';
+
+export default {
+ data() {
+ return {
+ remainingTime: formatTime(0),
+ remainingTimeIntervalId: null,
+ };
+ },
+
+ mounted() {
+ this.startRemainingTimeInterval();
+ },
+
+ beforeDestroy() {
+ if (this.remainingTimeIntervalId) {
+ clearInterval(this.remainingTimeIntervalId);
+ }
+ },
+
+ computed: {
+ isDelayedJob() {
+ return this.job && this.job.scheduled;
+ },
+ },
+
+ watch: {
+ isDelayedJob() {
+ this.startRemainingTimeInterval();
+ },
+ },
+
+ methods: {
+ startRemainingTimeInterval() {
+ if (this.remainingTimeIntervalId) {
+ clearInterval(this.remainingTimeIntervalId);
+ }
+
+ if (this.isDelayedJob) {
+ this.updateRemainingTime();
+ this.remainingTimeIntervalId = setInterval(() => this.updateRemainingTime(), 1000);
+ }
+ },
+
+ updateRemainingTime() {
+ const remainingMilliseconds = calculateRemainingMilliseconds(this.job.scheduled_at);
+ this.remainingTime = formatTime(remainingMilliseconds);
+ },
+ },
+};
diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js
index 54ed217572a..8045f6dc3ff 100644
--- a/app/assets/javascripts/jobs/store/actions.js
+++ b/app/assets/javascripts/jobs/store/actions.js
@@ -80,8 +80,8 @@ export const fetchJob = ({ state, dispatch }) => {
export const receiveJobSuccess = ({ commit }, data = {}) => {
commit(types.RECEIVE_JOB_SUCCESS, data);
- if (data.favicon) {
- setFaviconOverlay(data.favicon);
+ if (data.status && data.status.favicon) {
+ setFaviconOverlay(data.status.favicon);
} else {
resetFavicon();
}
diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js
index 4de01f8e532..d440b2c9ef1 100644
--- a/app/assets/javascripts/jobs/store/getters.js
+++ b/app/assets/javascripts/jobs/store/getters.js
@@ -35,16 +35,19 @@ export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status);
* Used to check if it should render the job log or the empty state
* @returns {Boolean}
*/
-export const hasTrace = state => state.job.has_trace || (!_.isEmpty(state.job.status) && state.job.status.group === 'running');
+export const hasTrace = state =>
+ state.job.has_trace || (!_.isEmpty(state.job.status) && state.job.status.group === 'running');
export const emptyStateIllustration = state =>
(state.job && state.job.status && state.job.status.illustration) || {};
-export const emptyStateAction = state => (state.job && state.job.status && state.job.status.action) || {};
+export const emptyStateAction = state =>
+ (state.job && state.job.status && state.job.status.action) || {};
export const isScrollingDown = state => isScrolledToBottom() && !state.isTraceComplete;
-export const hasRunnersForProject = state => state.job.runners.available && !state.job.runners.online;
+export const hasRunnersForProject = state =>
+ state.job.runners.available && !state.job.runners.online;
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 3c38d998b6c..c0a76814102 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -25,16 +25,43 @@ export default class LabelsSelect {
}
$els.each(function(i, dropdown) {
- var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer;
+ var $block,
+ $colorPreview,
+ $dropdown,
+ $form,
+ $loading,
+ $selectbox,
+ $sidebarCollapsedValue,
+ $value,
+ abilityName,
+ defaultLabel,
+ enableLabelCreateButton,
+ issueURLSplit,
+ issueUpdateURL,
+ labelUrl,
+ namespacePath,
+ projectPath,
+ saveLabelData,
+ selectedLabel,
+ showAny,
+ showNo,
+ $sidebarLabelTooltip,
+ initialSelected,
+ $toggleText,
+ fieldName,
+ useId,
+ propertyName,
+ showMenuAbove,
+ $container,
+ $dropdownContainer;
$dropdown = $(dropdown);
$dropdownContainer = $dropdown.closest('.labels-filter');
$toggleText = $dropdown.find('.dropdown-toggle-text');
namespacePath = $dropdown.data('namespacePath');
projectPath = $dropdown.data('projectPath');
- labelUrl = $dropdown.data('labels');
issueUpdateURL = $dropdown.data('issueUpdate');
selectedLabel = $dropdown.data('selected');
- if ((selectedLabel != null) && !$dropdown.hasClass('js-multiselect')) {
+ if (selectedLabel != null && !$dropdown.hasClass('js-multiselect')) {
selectedLabel = selectedLabel.split(',');
}
showNo = $dropdown.data('showNo');
@@ -50,26 +77,37 @@ export default class LabelsSelect {
$value = $block.find('.value');
$loading = $block.find('.block-loading').fadeOut();
fieldName = $dropdown.data('fieldName');
- useId = $dropdown.is('.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown');
+ useId = $dropdown.is(
+ '.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown',
+ );
propertyName = useId ? 'id' : 'title';
initialSelected = $selectbox
.find('input[name="' + $dropdown.data('fieldName') + '"]')
- .map(function () {
+ .map(function() {
return this.value;
- }).get();
+ })
+ .get();
const { handleClick } = options;
$sidebarLabelTooltip.tooltip();
if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
- new CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath);
+ new CreateLabelDropdown(
+ $dropdown.closest('.dropdown').find('.dropdown-new-label'),
+ namespacePath,
+ projectPath,
+ );
}
saveLabelData = function() {
var data, selected;
- selected = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "']").map(function() {
- return this.value;
- }).get();
+ selected = $dropdown
+ .closest('.selectbox')
+ .find("input[name='" + fieldName + "']")
+ .map(function() {
+ return this.value;
+ })
+ .get();
if (_.isEqual(initialSelected, selected)) return;
initialSelected = selected;
@@ -82,7 +120,8 @@ export default class LabelsSelect {
}
$loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
- axios.put(issueUpdateURL, data)
+ axios
+ .put(issueUpdateURL, data)
.then(({ data }) => {
var labelCount, template, labelTooltipTitle, labelTitles, formattedLabels;
$loading.fadeOut();
@@ -96,8 +135,7 @@ export default class LabelsSelect {
issueUpdateURL,
});
labelCount = data.labels.length;
- }
- else {
+ } else {
template = '<span class="no-value">None</span>';
}
$value.removeAttr('style').html(template);
@@ -114,17 +152,14 @@ export default class LabelsSelect {
}
labelTooltipTitle = labelTitles.join(', ');
- }
- else {
+ } else {
labelTooltipTitle = __('Labels');
}
- $sidebarLabelTooltip
- .attr('title', labelTooltipTitle)
- .tooltip('_fixTitle');
+ $sidebarLabelTooltip.attr('title', labelTooltipTitle).tooltip('_fixTitle');
$('.has-tooltip', $value).tooltip({
- container: 'body'
+ container: 'body',
});
})
.catch(() => flash(__('Error saving label update.')));
@@ -132,34 +167,39 @@ export default class LabelsSelect {
$dropdown.glDropdown({
showMenuAbove: showMenuAbove,
data: function(term, callback) {
- axios.get(labelUrl)
- .then((res) => {
- let data = _.chain(res.data).groupBy(function(label) {
- return label.title;
- }).map(function(label) {
- var color;
- color = _.map(label, function(dup) {
- return dup.color;
- });
- return {
- id: label[0].id,
- title: label[0].title,
- color: color,
- duplicate: color.length > 1
- };
- }).value();
+ labelUrl = $dropdown.attr('data-labels');
+ axios
+ .get(labelUrl)
+ .then(res => {
+ let data = _.chain(res.data)
+ .groupBy(function(label) {
+ return label.title;
+ })
+ .map(function(label) {
+ var color;
+ color = _.map(label, function(dup) {
+ return dup.color;
+ });
+ return {
+ id: label[0].id,
+ title: label[0].title,
+ color: color,
+ duplicate: color.length > 1,
+ };
+ })
+ .value();
if ($dropdown.hasClass('js-extra-options')) {
var extraData = [];
if (showNo) {
extraData.unshift({
id: 0,
- title: 'No Label'
+ title: 'No Label',
});
}
if (showAny) {
extraData.unshift({
isAny: true,
- title: 'Any Label'
+ title: 'Any Label',
});
}
if (extraData.length) {
@@ -176,11 +216,22 @@ export default class LabelsSelect {
.catch(() => flash(__('Error fetching labels.')));
},
renderRow: function(label, instance) {
- var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue;
+ var $a,
+ $li,
+ color,
+ colorEl,
+ indeterminate,
+ removesAll,
+ selectedClass,
+ spacing,
+ i,
+ marked,
+ dropdownName,
+ dropdownValue;
$li = $('<li>');
$a = $('<a href="#">');
selectedClass = [];
- removesAll = label.id <= 0 || (label.id == null);
+ removesAll = label.id <= 0 || label.id == null;
if ($dropdown.hasClass('js-filter-bulk-update')) {
indeterminate = $dropdown.data('indeterminate') || [];
marked = $dropdown.data('marked') || [];
@@ -200,9 +251,19 @@ export default class LabelsSelect {
} else {
if (this.id(label)) {
dropdownName = $dropdown.data('fieldName');
- dropdownValue = this.id(label).toString().replace(/'/g, '\\\'');
-
- if ($form.find("input[type='hidden'][name='" + dropdownName + "'][value='" + dropdownValue + "']").length) {
+ dropdownValue = this.id(label)
+ .toString()
+ .replace(/'/g, "\\'");
+
+ if (
+ $form.find(
+ "input[type='hidden'][name='" +
+ dropdownName +
+ "'][value='" +
+ dropdownValue +
+ "']",
+ ).length
+ ) {
selectedClass.push('is-active');
}
}
@@ -213,16 +274,14 @@ export default class LabelsSelect {
}
if (label.duplicate) {
color = DropdownUtils.duplicateLabelColor(label.color);
- }
- else {
+ } else {
if (label.color != null) {
[color] = label.color;
}
}
if (color) {
colorEl = "<span class='dropdown-label-box' style='background: " + color + "'></span>";
- }
- else {
+ } else {
colorEl = '';
}
// We need to identify which items are actually labels
@@ -235,7 +294,7 @@ export default class LabelsSelect {
return $li.html($a).prop('outerHTML');
},
search: {
- fields: ['title']
+ fields: ['title'],
},
selectable: true,
filterable: true,
@@ -255,25 +314,21 @@ export default class LabelsSelect {
if (selected && selected.id === 0) {
this.selected = [];
return 'No Label';
- }
- else if (isSelected) {
+ } else if (isSelected) {
this.selected.push(title);
- }
- else if (!isSelected && title) {
+ } else if (!isSelected && title) {
var index = this.selected.indexOf(title);
this.selected.splice(index, 1);
}
if (selectedLabels.length === 1) {
return selectedLabels;
- }
- else if (selectedLabels.length) {
+ } else if (selectedLabels.length) {
return sprintf(__('%{firstLabel} +%{labelCount} more'), {
firstLabel: selectedLabels[0],
- labelCount: selectedLabels.length - 1
+ labelCount: selectedLabels.length - 1,
});
- }
- else {
+ } else {
return defaultLabel;
}
},
@@ -285,10 +340,9 @@ export default class LabelsSelect {
return label.id;
}
- if ($dropdown.hasClass("js-filter-submit") && (label.isAny == null)) {
+ if ($dropdown.hasClass('js-filter-submit') && label.isAny == null) {
return label.title;
- }
- else {
+ } else {
return label.id;
}
},
@@ -310,13 +364,13 @@ export default class LabelsSelect {
}
if ($dropdown.hasClass('js-multiselect')) {
if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
- selectedLabels = $dropdown.closest('form').find("input:hidden[name='" + ($dropdown.data('fieldName')) + "']");
+ selectedLabels = $dropdown
+ .closest('form')
+ .find("input:hidden[name='" + $dropdown.data('fieldName') + "']");
Issuable.filterResults($dropdown.closest('form'));
- }
- else if ($dropdown.hasClass('js-filter-submit')) {
+ } else if ($dropdown.hasClass('js-filter-submit')) {
$dropdown.closest('form').submit();
- }
- else {
+ } else {
if (!$dropdown.hasClass('js-filter-bulk-update')) {
saveLabelData();
}
@@ -325,7 +379,7 @@ export default class LabelsSelect {
},
multiSelect: $dropdown.hasClass('js-multiselect'),
vue: $dropdown.hasClass('js-issue-board-sidebar'),
- clicked: function (clickEvent) {
+ clicked: function(clickEvent) {
const { $el, e, isMarking } = clickEvent;
const label = clickEvent.selectedObj;
@@ -339,7 +393,8 @@ export default class LabelsSelect {
isMRIndex = page === 'projects:merge_requests:index';
if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) {
- $dropdown.parent()
+ $dropdown
+ .parent()
.find('.dropdown-clear-active')
.removeClass('is-active');
}
@@ -367,28 +422,26 @@ export default class LabelsSelect {
e.preventDefault();
return;
- }
- else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+ } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
if (!$dropdown.hasClass('js-multiselect')) {
selectedLabel = label.title;
return Issuable.filterResults($dropdown.closest('form'));
}
- }
- else if ($dropdown.hasClass('js-filter-submit')) {
+ } else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit();
- }
- else if ($dropdown.hasClass('js-issue-board-sidebar')) {
+ } else if ($dropdown.hasClass('js-issue-board-sidebar')) {
if ($el.hasClass('is-active')) {
- boardsStore.detail.issue.labels.push(new ListLabel({
- id: label.id,
- title: label.title,
- color: label.color[0],
- textColor: '#fff'
- }));
- }
- else {
+ boardsStore.detail.issue.labels.push(
+ new ListLabel({
+ id: label.id,
+ title: label.title,
+ color: label.color[0],
+ textColor: '#fff',
+ }),
+ );
+ } else {
var { labels } = boardsStore.detail.issue;
- labels = labels.filter(function (selectedLabel) {
+ labels = labels.filter(function(selectedLabel) {
return selectedLabel.id !== label.id;
});
boardsStore.detail.issue.labels = labels;
@@ -396,19 +449,16 @@ export default class LabelsSelect {
$loading.fadeIn();
- boardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
+ boardsStore.detail.issue
+ .update($dropdown.attr('data-issue-update'))
.then(fadeOutLoader)
.catch(fadeOutLoader);
- }
- else if (handleClick) {
+ } else if (handleClick) {
e.preventDefault();
handleClick(label);
- }
- else {
+ } else {
if ($dropdown.hasClass('js-multiselect')) {
-
- }
- else {
+ } else {
return saveLabelData();
}
}
@@ -436,15 +486,17 @@ export default class LabelsSelect {
// so best approach is to use traditional way of
// concatenation
// see: http://2ality.com/2016/05/template-literal-whitespace.html#joining-arrays
- const tpl = _.template([
- '<% _.each(labels, function(label){ %>',
- '<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">',
- '<span class="badge label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">',
- '<%- label.title %>',
- '</span>',
- '</a>',
- '<% }); %>',
- ].join(''));
+ const tpl = _.template(
+ [
+ '<% _.each(labels, function(label){ %>',
+ '<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">',
+ '<span class="badge label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">',
+ '<%- label.title %>',
+ '</span>',
+ '</a>',
+ '<% }); %>',
+ ].join(''),
+ );
return tpl(tplData);
}
diff --git a/app/assets/javascripts/lib/utils/ace_utils.js b/app/assets/javascripts/lib/utils/ace_utils.js
index efc4b2a8d94..ee71ae0e61a 100644
--- a/app/assets/javascripts/lib/utils/ace_utils.js
+++ b/app/assets/javascripts/lib/utils/ace_utils.js
@@ -1,6 +1,6 @@
/* global ace */
export default function getModeByFileExtension(path) {
- const modelist = ace.require("ace/ext/modelist");
+ const modelist = ace.require('ace/ext/modelist');
return modelist.getModeForPath(path).mode;
-};
+}
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 46740308f17..59007d5950e 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -14,7 +14,7 @@ window.timeago = timeago;
*
* @param {Boolean} abbreviated
*/
-const getMonthNames = abbreviated => {
+export const getMonthNames = abbreviated => {
if (abbreviated) {
return [
s__('Jan'),
@@ -454,12 +454,20 @@ export const parseSeconds = (seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {})
/**
* Accepts a timeObject (see parseSeconds) and returns a condensed string representation of it
* (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included.
+ * If the 'fullNameFormat' param is passed it returns a non condensed string eg '1 week 3 days'
*/
-export const stringifyTime = timeObject => {
+export const stringifyTime = (timeObject, fullNameFormat = false) => {
const reducedTime = _.reduce(
timeObject,
(memo, unitValue, unitName) => {
const isNonZero = !!unitValue;
+
+ if (fullNameFormat && isNonZero) {
+ // Remove traling 's' if unit value is singular
+ const formatedUnitName = unitValue > 1 ? unitName : unitName.replace(/s$/, '');
+ return `${memo} ${unitValue} ${formatedUnitName}`;
+ }
+
return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo;
},
'',
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
index afbab59055b..2ccc51c35f7 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -7,7 +7,7 @@ import { BYTES_IN_KIB } from './constants';
* * * Show 3 digits to the right
* * For 2 digits to the left of the decimal point and X digits to the right of it
* * * Show 2 digits to the right
-*/
+ */
export function formatRelevantDigits(number) {
let digitsLeft = '';
let relevantDigits = 0;
diff --git a/app/assets/javascripts/members.js b/app/assets/javascripts/members.js
index 7d0c701fd70..bd263c75a3d 100644
--- a/app/assets/javascripts/members.js
+++ b/app/assets/javascripts/members.js
@@ -7,8 +7,12 @@ export default class Members {
}
addListeners() {
- $('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this));
- $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this));
+ $('.js-member-update-control')
+ .off('change')
+ .on('change', this.formSubmit.bind(this));
+ $('.js-edit-member-form')
+ .off('ajax:success')
+ .on('ajax:success', this.formSuccess.bind(this));
gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
}
@@ -28,7 +32,7 @@ export default class Members {
toggleLabel(selected, $el) {
return $el.text();
},
- clicked: (options) => {
+ clicked: options => {
this.formSubmit(null, options.$el);
},
});
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 2950c2299ab..d8255181574 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -11,7 +11,6 @@ import bp from './breakpoints';
import { parseUrlPathname, handleLocationHash, isMetaClick } from './lib/utils/common_utils';
import { isInVueNoteablePage } from './lib/utils/dom_utils';
import { getLocationHash } from './lib/utils/url_utility';
-import initDiscussionTab from './image_diff/init_discussion_tab';
import Diff from './diff';
import { localTimeAgo } from './lib/utils/datetime_utility';
import syntaxHighlight from './syntax_highlight';
@@ -207,8 +206,6 @@ export default class MergeRequestTabs {
}
this.resetViewContainer();
this.destroyPipelinesView();
-
- initDiscussionTab();
}
if (this.setUrl) {
this.setCurrentAction(action);
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 42fb5c7177a..d32f39881dd 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -9,7 +9,10 @@ import '~/gl_dropdown';
import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility';
import ModalStore from './boards/stores/modal_store';
-import boardsStore, { boardStoreIssueSet, boardStoreIssueDelete } from './boards/stores/boards_store';
+import boardsStore, {
+ boardStoreIssueSet,
+ boardStoreIssueDelete,
+} from './boards/stores/boards_store';
export default class MilestoneSelect {
constructor(currentProject, els, options = {}) {
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index b980e43b898..10e80883c00 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -6,7 +6,6 @@ import Autosize from 'autosize';
import { __, sprintf } from '~/locale';
import Flash from '../../flash';
import Autosave from '../../autosave';
-import TaskList from '../../task_list';
import {
capitalizeFirstCharacter,
convertToCamelCase,
@@ -146,7 +145,6 @@ export default {
});
this.initAutoSave();
- this.initTaskList();
},
methods: {
...mapActions([
@@ -298,13 +296,6 @@ Please check your network connection and try again.`;
]);
}
},
- initTaskList() {
- return new TaskList({
- dataType: 'note',
- fieldName: 'note',
- selector: '.notes',
- });
- },
resizeTextarea() {
this.$nextTick(() => {
Autosize.update(this.$refs.textarea);
@@ -321,10 +312,10 @@ Please check your network connection and try again.`;
v-else-if="!canCreateNote"
:issuable-type="issuableTypeTitle"
/>
- <ul
+ <div
v-else-if="canCreateNote"
class="notes notes-form timeline">
- <li class="timeline-entry">
+ <div class="timeline-entry note-form">
<div class="timeline-entry-inner">
<div class="flash-container error-alert timeline-content"></div>
<div class="timeline-icon d-none d-sm-none d-md-block">
@@ -390,7 +381,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
:disabled="isSubmitButtonDisabled"
name="button"
type="button"
- class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle"
+ class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown"
data-display="static"
data-toggle="dropdown"
aria-label="Open comment type dropdown">
@@ -422,7 +413,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
<li :class="{ 'droplab-item-selected': noteType === 'discussion' }">
<button
type="button"
- class="btn btn-transparent"
+ class="btn btn-transparent qa-discussion-option"
@click.prevent="setNoteType('discussion')">
<i
aria-hidden="true"
@@ -462,7 +453,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
</form>
</div>
</div>
- </li>
- </ul>
+ </div>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue
index eaa0cded224..080161dfbba 100644
--- a/app/assets/javascripts/notes/components/diff_with_note.vue
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -1,15 +1,17 @@
<script>
import { mapState, mapActions } from 'vuex';
-import imageDiffHelper from '~/image_diff/helpers/index';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
+import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
+import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
import { GlSkeletonLoading } from '@gitlab-org/gitlab-ui';
-import { trimFirstCharOfLineContent } from '~/diffs/store/utils';
+import { trimFirstCharOfLineContent, getDiffMode } from '~/diffs/store/utils';
export default {
components: {
DiffFileHeader,
GlSkeletonLoading,
+ DiffViewer,
+ ImageDiffOverlay,
},
props: {
discussion: {
@@ -25,9 +27,15 @@ export default {
computed: {
...mapState({
noteableData: state => state.notes.noteableData,
+ projectPath: state => state.diffs.projectPath,
}),
+ diffMode() {
+ return getDiffMode(this.diffFile);
+ },
hasTruncatedDiffLines() {
- return this.discussion.truncatedDiffLines && this.discussion.truncatedDiffLines.length !== 0;
+ return (
+ this.discussion.truncated_diff_lines && this.discussion.truncated_diff_lines.length !== 0
+ );
},
isDiscussionsExpanded() {
return true; // TODO: @fatihacet - Fix this.
@@ -43,30 +51,24 @@ export default {
return text ? 'text-file' : 'js-image-file';
},
diffFile() {
- return convertObjectPropsToCamelCase(this.discussion.diffFile, { deep: true });
+ return this.discussion.diff_file;
},
imageDiffHtml() {
- return this.discussion.imageDiffHtml;
+ return this.discussion.image_diff_html;
},
userColorScheme() {
return window.gon.user_color_scheme;
},
normalizedDiffLines() {
- if (this.discussion.truncatedDiffLines) {
- return this.discussion.truncatedDiffLines.map(line =>
- trimFirstCharOfLineContent(convertObjectPropsToCamelCase(line)),
- );
+ if (this.discussion.truncated_diff_lines) {
+ return this.discussion.truncated_diff_lines.map(line => trimFirstCharOfLineContent(line));
}
return [];
},
},
mounted() {
- if (this.isImageDiff) {
- const canCreateNote = false;
- const renderCommentBadge = true;
- imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge);
- } else if (!this.hasTruncatedDiffLines) {
+ if (!this.hasTruncatedDiffLines) {
this.fetchDiff();
}
},
@@ -94,7 +96,7 @@ export default {
class="diff-file file-holder"
>
<diff-file-header
- :discussion-path="discussion.discussionPath"
+ :discussion-path="discussion.discussion_path"
:diff-file="diffFile"
:can-current-user-fork="false"
:discussions-expanded="isDiscussionsExpanded"
@@ -108,15 +110,15 @@ export default {
<table>
<tr
v-for="line in normalizedDiffLines"
- :key="line.lineCode"
+ :key="line.line_code"
class="line_holder"
>
- <td class="diff-line-num old_line">{{ line.oldLine }}</td>
- <td class="diff-line-num new_line">{{ line.newLine }}</td>
+ <td class="diff-line-num old_line">{{ line.old_line }}</td>
+ <td class="diff-line-num new_line">{{ line.new_line }}</td>
<td
:class="line.type"
class="line_content"
- v-html="line.richText"
+ v-html="line.rich_text"
>
</td>
</tr>
@@ -160,7 +162,24 @@ export default {
<div
v-else
>
- <div v-html="imageDiffHtml"></div>
+ <diff-viewer
+ :diff-mode="diffMode"
+ :new-path="diffFile.new_path"
+ :new-sha="diffFile.diff_refs.head_sha"
+ :old-path="diffFile.old_path"
+ :old-sha="diffFile.diff_refs.base_sha"
+ :file-hash="diffFile.file_hash"
+ :project-path="projectPath"
+ >
+ <image-diff-overlay
+ slot="image-overlay"
+ :discussions="discussion"
+ :file-hash="diffFile.file_hash"
+ :show-comment-icon="true"
+ :should-toggle-discussion="false"
+ badge-class="image-comment-badge"
+ />
+ </diff-viewer>
<slot></slot>
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index 1f80f24e045..a4d76a70696 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -1,9 +1,6 @@
<script>
import { mapActions, mapGetters } from 'vuex';
-import resolveSvg from 'icons/_icon_resolve_discussion.svg';
-import resolvedSvg from 'icons/_icon_status_success_solid.svg';
-import mrIssueSvg from 'icons/_icon_mr_issue.svg';
-import nextDiscussionSvg from 'icons/_next_discussion.svg';
+import Icon from '~/vue_shared/components/icon.vue';
import { pluralize } from '../../lib/utils/text_utility';
import discussionNavigation from '../mixins/discussion_navigation';
import tooltip from '../../vue_shared/directives/tooltip';
@@ -12,6 +9,9 @@ export default {
directives: {
tooltip,
},
+ components: {
+ Icon,
+ },
mixins: [discussionNavigation],
computed: {
...mapGetters([
@@ -37,12 +37,6 @@ export default {
return this.getNoteableData.create_issue_to_resolve_discussions_path;
},
},
- created() {
- this.resolveSvg = resolveSvg;
- this.resolvedSvg = resolvedSvg;
- this.mrIssueSvg = mrIssueSvg;
- this.nextDiscussionSvg = nextDiscussionSvg;
- },
methods: {
...mapActions(['expandDiscussion']),
jumpToFirstUnresolvedDiscussion() {
@@ -66,15 +60,9 @@ export default {
<span
:class="{ 'is-active': allResolved }"
class="line-resolve-btn is-disabled"
- type="button">
- <span
- v-if="allResolved"
- v-html="resolvedSvg"
- ></span>
- <span
- v-else
- v-html="resolveSvg"
- ></span>
+ type="button"
+ >
+ <icon name="check-circle" />
</span>
<span class="line-resolve-text">
{{ resolvedDiscussionCount }}/{{ discussionCount }} {{ countText }} resolved
@@ -90,7 +78,7 @@ export default {
:title="s__('Resolve all discussions in new issue')"
data-container="body"
class="new-issue-for-discussion btn btn-default discussion-create-issue-btn">
- <span v-html="mrIssueSvg"></span>
+ <icon name="issue-new" />
</a>
</div>
<div
@@ -103,7 +91,7 @@ export default {
data-container="body"
class="btn btn-default discussion-next-btn"
@click="jumpToFirstUnresolvedDiscussion">
- <span v-html="nextDiscussionSvg"></span>
+ <icon name="comment-next" />
</button>
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue
index 6e8f43048d1..affa2d1b574 100644
--- a/app/assets/javascripts/notes/components/discussion_filter.vue
+++ b/app/assets/javascripts/notes/components/discussion_filter.vue
@@ -1,7 +1,8 @@
<script>
import $ from 'jquery';
-import Icon from '~/vue_shared/components/icon.vue';
import { mapGetters, mapActions } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import { DISCUSSION_FILTERS_DEFAULT_VALUE, HISTORY_ONLY_FILTER_VALUE } from '../constants';
export default {
components: {
@@ -12,14 +13,17 @@ export default {
type: Array,
required: true,
},
- defaultValue: {
+ selectedValue: {
type: Number,
default: null,
required: false,
},
},
data() {
- return { currentValue: this.defaultValue };
+ return {
+ currentValue: this.selectedValue,
+ defaultValue: DISCUSSION_FILTERS_DEFAULT_VALUE,
+ };
},
computed: {
...mapGetters(['getNotesDataByProp']),
@@ -28,8 +32,11 @@ export default {
return this.filters.find(filter => filter.value === this.currentValue);
},
},
+ mounted() {
+ this.toggleCommentsForm();
+ },
methods: {
- ...mapActions(['filterDiscussion']),
+ ...mapActions(['filterDiscussion', 'setCommentsDisabled']),
selectFilter(value) {
const filter = parseInt(value, 10);
@@ -39,6 +46,10 @@ export default {
if (filter === this.currentValue) return;
this.currentValue = filter;
this.filterDiscussion({ path: this.getNotesDataByProp('discussionsPath'), filter });
+ this.toggleCommentsForm();
+ },
+ toggleCommentsForm() {
+ this.setCommentsDisabled(this.currentValue === HISTORY_ONLY_FILTER_VALUE);
},
},
};
@@ -73,6 +84,10 @@ export default {
>
{{ filter.title }}
</button>
+ <div
+ v-if="filter.value === defaultValue"
+ class="dropdown-divider"
+ ></div>
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index e075f94b82b..f7a61fbfcd4 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -1,19 +1,14 @@
<script>
import { mapGetters } from 'vuex';
-import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
-import emojiSmile from 'icons/_emoji_smile.svg';
-import emojiSmiley from 'icons/_emoji_smiley.svg';
-import editSvg from 'icons/_icon_pencil.svg';
-import resolveDiscussionSvg from 'icons/_icon_resolve_discussion.svg';
-import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg';
-import ellipsisSvg from 'icons/_ellipsis_v.svg';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
name: 'NoteActions',
components: {
Icon,
+ GlLoadingIcon,
},
directives: {
tooltip,
@@ -108,15 +103,6 @@ export default {
return title;
},
},
- created() {
- this.emojiSmiling = emojiSmiling;
- this.emojiSmile = emojiSmile;
- this.emojiSmiley = emojiSmiley;
- this.editSvg = editSvg;
- this.ellipsisSvg = ellipsisSvg;
- this.resolveDiscussionSvg = resolveDiscussionSvg;
- this.resolvedDiscussionSvg = resolvedDiscussionSvg;
- },
methods: {
onEdit() {
this.$emit('handleEdit');
@@ -150,12 +136,7 @@ export default {
class="line-resolve-btn note-action-button"
@click="onResolve">
<template v-if="!isResolving">
- <div
- v-if="isResolved"
- v-html="resolvedDiscussionSvg"></div>
- <div
- v-else
- v-html="resolveDiscussionSvg"></div>
+ <icon name="check-circle" />
</template>
<gl-loading-icon
v-else
@@ -177,18 +158,18 @@ export default {
title="Add reaction"
>
<gl-loading-icon inline/>
- <span
- class="link-highlight award-control-icon-neutral"
- v-html="emojiSmiling">
- </span>
- <span
- class="link-highlight award-control-icon-positive"
- v-html="emojiSmiley">
- </span>
- <span
- class="link-highlight award-control-icon-super-positive"
- v-html="emojiSmile">
- </span>
+ <icon
+ css-classes="link-highlight award-control-icon-neutral"
+ name="emoji_slightly_smiling_face"
+ />
+ <icon
+ css-classes="link-highlight award-control-icon-positive"
+ name="emoji_smiley"
+ />
+ <icon
+ css-classes="link-highlight award-control-icon-super-positive"
+ name="emoji_smiley"
+ />
</a>
</div>
<div
@@ -202,10 +183,10 @@ export default {
data-container="body"
data-placement="bottom"
@click="onEdit">
- <span
- class="link-highlight"
- v-html="editSvg">
- </span>
+ <icon
+ name="pencil"
+ css-classes="link-highlight"
+ />
</button>
</div>
<div
@@ -238,15 +219,15 @@ export default {
data-toggle="dropdown"
data-container="body"
data-placement="bottom">
- <span
- class="icon"
- v-html="ellipsisSvg">
- </span>
+ <icon
+ css-classes="icon"
+ name="ellipsis_v"
+ />
</button>
<ul class="dropdown-menu more-actions-dropdown dropdown-open-left">
<li v-if="canReportAsAbuse">
<a :href="reportAbusePath">
- Report as abuse
+ {{ __('Report abuse to GitLab') }}
</a>
</li>
<li v-if="noteUrl">
@@ -255,7 +236,7 @@ export default {
type="button"
class="btn-default btn-transparent js-btn-copy-note-link"
>
- Copy link
+ {{ __('Copy link') }}
</button>
</li>
<li v-if="canEdit">
@@ -264,7 +245,7 @@ export default {
type="button"
@click.prevent="onDelete">
<span class="text-danger">
- Delete comment
+ {{ __('Delete comment') }}
</span>
</button>
</li>
diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue
index e707f44bf5a..401bcfabbe4 100644
--- a/app/assets/javascripts/notes/components/note_awards_list.vue
+++ b/app/assets/javascripts/notes/components/note_awards_list.vue
@@ -1,13 +1,14 @@
<script>
import { mapActions, mapGetters } from 'vuex';
-import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
-import emojiSmile from 'icons/_emoji_smile.svg';
-import emojiSmiley from 'icons/_emoji_smiley.svg';
+import Icon from '~/vue_shared/components/icon.vue';
import Flash from '../../flash';
import { glEmojiTag } from '../../emoji';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
+ components: {
+ Icon,
+ },
directives: {
tooltip,
},
@@ -72,11 +73,6 @@ export default {
return this.noteAuthorId === this.getUserData.id;
},
},
- created() {
- this.emojiSmiling = emojiSmiling;
- this.emojiSmile = emojiSmile;
- this.emojiSmiley = emojiSmiley;
- },
methods: {
...mapActions(['toggleAwardRequest']),
getAwardHTML(name) {
@@ -110,7 +106,7 @@ export default {
// Get the remaining list to use in `and x more` text.
const remainingAwardList = awardList.slice(TOOLTIP_NAME_COUNT, awardList.length);
- // Add myself to the begining of the list so title will start with You.
+ // Add myself to the beginning of the list so title will start with You.
if (hasReactionByCurrentUser) {
namesToShow.unshift('You');
}
@@ -196,17 +192,14 @@ export default {
data-boundary="viewport"
data-placement="bottom"
type="button">
- <span
- class="award-control-icon award-control-icon-neutral"
- v-html="emojiSmiling">
+ <span class="award-control-icon award-control-icon-neutral">
+ <icon name="emoji_slightly_smiling_face" />
</span>
- <span
- class="award-control-icon award-control-icon-positive"
- v-html="emojiSmiley">
+ <span class="award-control-icon award-control-icon-positive">
+ <icon name="emoji_smiley" />
</span>
- <span
- class="award-control-icon award-control-icon-super-positive"
- v-html="emojiSmile">
+ <span class="award-control-icon award-control-icon-super-positive">
+ <icon name="emoji_smiley" />
</span>
<i
aria-hidden="true"
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index cf4c35de42c..9375627359c 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -4,7 +4,6 @@ import noteEditedText from './note_edited_text.vue';
import noteAwardsList from './note_awards_list.vue';
import noteAttachment from './note_attachment.vue';
import noteForm from './note_form.vue';
-import TaskList from '../../task_list';
import autosave from '../mixins/autosave';
export default {
@@ -37,14 +36,12 @@ export default {
},
mounted() {
this.renderGFM();
- this.initTaskList();
if (this.isEditing) {
this.initAutoSave(this.note);
}
},
updated() {
- this.initTaskList();
this.renderGFM();
if (this.isEditing) {
@@ -59,15 +56,6 @@ export default {
renderGFM() {
$(this.$refs['note-body']).renderGFM();
},
- initTaskList() {
- if (this.canEdit) {
- this.taskList = new TaskList({
- dataType: 'note',
- fieldName: 'note',
- selector: '.notes',
- });
- }
- },
handleFormUpdate(note, parentElement, callback) {
this.$emit('handleFormUpdate', note, parentElement, callback);
},
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 38c43e5fe08..31ee8fed984 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -187,7 +187,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"
+js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input"
aria-label="Description"
placeholder="Write a comment or drag your files here…"
@keydown.meta.enter="handleUpdate()"
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 7b6e7b72caf..dd7313d7b10 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -45,6 +45,9 @@ export default {
noteTimestampLink() {
return `#note_${this.noteId}`;
},
+ hasAuthor() {
+ return this.author && Object.keys(this.author).length;
+ },
},
methods: {
...mapActions(['setTargetNoteHash']),
@@ -76,7 +79,7 @@ export default {
</button>
</div>
<a
- v-if="Object.keys(author).length"
+ v-if="hasAuthor"
:href="author.path"
>
<span class="note-header-author-name">{{ author.name }}</span>
@@ -92,9 +95,6 @@ export default {
</span>
<span class="note-headline-light">
<span class="note-headline-meta">
- <template v-if="actionText">
- {{ actionText }}
- </template>
<span class="system-note-message">
<slot></slot>
</span>
@@ -102,7 +102,9 @@ export default {
v-if="createdAt"
>
<span class="system-note-separator">
- &middot;
+ <template v-if="actionText">
+ {{ actionText }}
+ </template>
</span>
<a
:href="noteTimestampLink"
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index c5fdfa1d47c..7740967ccd5 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -1,16 +1,15 @@
<script>
import { mapActions, mapGetters } from 'vuex';
-import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg';
-import nextDiscussionsSvg from 'icons/_next_discussion.svg';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { truncateSha } from '~/lib/utils/text_utility';
-import systemNote from '~/vue_shared/components/notes/system_note.vue';
import { s__ } from '~/locale';
+import systemNote from '~/vue_shared/components/notes/system_note.vue';
+import icon from '~/vue_shared/components/icon.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 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';
@@ -26,6 +25,7 @@ import tooltip from '../../vue_shared/directives/tooltip';
export default {
name: 'NoteableDiscussion',
components: {
+ icon,
noteableNote,
diffWithNote,
userAvatarLink,
@@ -33,6 +33,7 @@ export default {
noteSignedOutWidget,
noteEditedText,
noteForm,
+ toggleRepliesWidget,
placeholderNote,
placeholderSystemNote,
systemNote,
@@ -46,11 +47,6 @@ export default {
type: Object,
required: true,
},
- renderHeader: {
- type: Boolean,
- required: false,
- default: true,
- },
renderDiffFile: {
type: Boolean,
required: false,
@@ -72,6 +68,7 @@ export default {
isReplying: false,
isResolving: false,
resolveAsThread: true,
+ isRepliesCollapsed: (!this.discussion.diff_discussion && this.discussion.resolved) || false,
};
},
computed: {
@@ -90,17 +87,16 @@ export default {
transformedDiscussion() {
return {
...this.discussion.notes[0],
- truncatedDiffLines: this.discussion.truncated_diff_lines || [],
- truncatedDiffLinesPath: this.discussion.truncated_diff_lines_path,
- diffFile: this.discussion.diff_file,
- diffDiscussion: this.discussion.diff_discussion,
- imageDiffHtml: this.discussion.image_diff_html,
+ truncated_diff_lines: this.discussion.truncated_diff_lines || [],
+ truncated_diff_lines_path: this.discussion.truncated_diff_lines_path,
+ diff_file: this.discussion.diff_file,
+ diff_discussion: this.discussion.diff_discussion,
active: this.discussion.active,
- discussionPath: this.discussion.discussion_path,
+ discussion_path: this.discussion.discussion_path,
resolved: this.discussion.resolved,
- resolvedBy: this.discussion.resolved_by,
- resolvedByPush: this.discussion.resolved_by_push,
- resolvedAt: this.discussion.resolved_at,
+ resolved_by: this.discussion.resolved_by,
+ resolved_by_push: this.discussion.resolved_by_push,
+ resolved_at: this.discussion.resolved_at,
};
},
author() {
@@ -112,6 +108,15 @@ export default {
newNotePath() {
return this.getNoteableData.create_note_path;
},
+ hasReplies() {
+ return this.discussion.notes.length > 1;
+ },
+ initialDiscussion() {
+ return this.discussion.notes.slice(0, 1)[0];
+ },
+ replies() {
+ return this.discussion.notes.slice(1);
+ },
lastUpdatedBy() {
const { notes } = this.discussion;
@@ -131,7 +136,7 @@ export default {
return null;
},
resolvedText() {
- return this.transformedDiscussion.resolvedByPush ? 'Automatically resolved' : 'Resolved';
+ return this.transformedDiscussion.resolved_by_push ? 'Automatically resolved' : 'Resolved';
},
hasMultipleUnresolvedDiscussions() {
return this.unresolvedDiscussions.length > 1;
@@ -143,16 +148,24 @@ export default {
);
},
shouldRenderDiffs() {
- const { diffDiscussion, diffFile } = this.transformedDiscussion;
-
- return diffDiscussion && diffFile && this.renderDiffFile;
+ return (
+ this.transformedDiscussion.diff_discussion &&
+ this.transformedDiscussion.diff_file &&
+ this.renderDiffFile
+ );
+ },
+ shouldGroupReplies() {
+ return !this.shouldRenderDiffs && !this.transformedDiscussion.diff_discussion;
+ },
+ shouldRenderHeader() {
+ return this.shouldRenderDiffs;
},
wrapperComponent() {
return this.shouldRenderDiffs ? diffWithNote : 'div';
},
wrapperComponentProps() {
if (this.shouldRenderDiffs) {
- return { discussion: convertObjectPropsToCamelCase(this.discussion) };
+ return { discussion: this.discussion };
}
return {};
@@ -160,6 +173,22 @@ export default {
wrapperClass() {
return this.isDiffDiscussion ? '' : 'card discussion-wrapper';
},
+ componentClassName() {
+ if (this.shouldRenderDiffs) {
+ if (!this.lastUpdatedAt && !this.discussion.resolved) {
+ return 'unresolved';
+ }
+ }
+
+ return '';
+ },
+ shouldShowDiscussions() {
+ const isExpanded = this.discussion.expanded;
+ const { resolved } = this.transformedDiscussion;
+ const isResolvedNonDiffDiscussion = !this.transformedDiscussion.diff_discussion && resolved;
+
+ return isExpanded || this.alwaysExpanded || isResolvedNonDiffDiscussion;
+ },
},
watch: {
isReplying() {
@@ -173,10 +202,6 @@ export default {
}
},
},
- created() {
- this.resolveDiscussionsSvg = resolveDiscussionsSvg;
- this.nextDiscussionsSvg = nextDiscussionsSvg;
- },
methods: {
...mapActions([
'saveNote',
@@ -207,6 +232,9 @@ export default {
toggleDiscussionHandler() {
this.toggleDiscussion({ discussionId: this.discussion.id });
},
+ toggleReplies() {
+ this.isRepliesCollapsed = !this.isRepliesCollapsed;
+ },
showReplyForm() {
this.isReplying = true;
},
@@ -274,26 +302,29 @@ Please check your network connection and try again.`;
</script>
<template>
- <li class="note note-discussion timeline-entry">
+ <li
+ class="note note-discussion timeline-entry"
+ :class="componentClassName"
+ >
<div class="timeline-entry-inner">
- <div class="timeline-icon">
- <user-avatar-link
- v-if="author"
- :link-href="author.path"
- :img-src="author.avatar_url"
- :img-alt="author.name"
- :img-size="40"
- />
- </div>
<div class="timeline-content">
<div
:data-discussion-id="transformedDiscussion.discussion_id"
class="discussion js-discussion-container"
>
<div
- v-if="renderHeader"
- class="discussion-header"
+ v-if="shouldRenderHeader"
+ class="discussion-header note-wrapper"
>
+ <div class="timeline-icon">
+ <user-avatar-link
+ v-if="author"
+ :link-href="author.path"
+ :img-src="author.avatar_url"
+ :img-alt="author.name"
+ :img-size="40"
+ />
+ </div>
<note-header
:author="author"
:created-at="transformedDiscussion.created_at"
@@ -302,9 +333,9 @@ Please check your network connection and try again.`;
:expanded="discussion.expanded"
@toggleHandler="toggleDiscussionHandler"
>
- <template v-if="transformedDiscussion.diffDiscussion">
+ <template v-if="transformedDiscussion.diff_discussion">
started a discussion on
- <a :href="transformedDiscussion.discussionPath">
+ <a :href="transformedDiscussion.discussion_path">
<template v-if="transformedDiscussion.active">
the diff
</template>
@@ -325,8 +356,8 @@ Please check your network connection and try again.`;
</note-header>
<note-edited-text
v-if="transformedDiscussion.resolved"
- :edited-at="transformedDiscussion.resolvedAt"
- :edited-by="transformedDiscussion.resolvedBy"
+ :edited-at="transformedDiscussion.resolved_at"
+ :edited-by="transformedDiscussion.resolved_by"
:action-text="resolvedText"
class-name="discussion-headline-light js-discussion-headline"
/>
@@ -339,7 +370,7 @@ Please check your network connection and try again.`;
/>
</div>
<div
- v-if="discussion.expanded || alwaysExpanded"
+ v-if="shouldShowDiscussions"
class="discussion-body">
<component
:is="wrapperComponent"
@@ -348,38 +379,70 @@ Please check your network connection and try again.`;
>
<div class="discussion-notes">
<ul class="notes">
- <component
- :is="componentName(note)"
- v-for="note in discussion.notes"
- :key="note.id"
- :note="componentData(note)"
- @handleDeleteNote="deleteNoteHandler"
- />
+ <template v-if="shouldGroupReplies">
+ <component
+ :is="componentName(initialDiscussion)"
+ :note="componentData(initialDiscussion)"
+ @handleDeleteNote="deleteNoteHandler"
+ >
+ <slot
+ slot="avatar-badge"
+ name="avatar-badge"
+ >
+ </slot>
+ </component>
+ <toggle-replies-widget
+ v-if="hasReplies"
+ :collapsed="isRepliesCollapsed"
+ :replies="replies"
+ @toggle="toggleReplies"
+ />
+ <template v-if="!isRepliesCollapsed">
+ <component
+ :is="componentName(note)"
+ v-for="note in replies"
+ :key="note.id"
+ :note="componentData(note)"
+ @handleDeleteNote="deleteNoteHandler"
+ />
+ </template>
+ </template>
+ <template v-else>
+ <component
+ :is="componentName(note)"
+ v-for="(note, index) in discussion.notes"
+ :key="note.id"
+ :note="componentData(note)"
+ @handleDeleteNote="deleteNoteHandler"
+ >
+ <slot
+ v-if="index === 0"
+ slot="avatar-badge"
+ name="avatar-badge"
+ >
+ </slot>
+ </component>
+ </template>
</ul>
<div
+ v-if="!isRepliesCollapsed"
:class="{ 'is-replying': isReplying }"
class="discussion-reply-holder"
>
<template v-if="!isReplying && canReply">
- <div
- class="btn-group d-flex discussion-with-resolve-btn"
- role="group">
- <div
- class="btn-group w-100"
- role="group">
- <button
- type="button"
- class="js-vue-discussion-reply btn btn-text-field mr-2"
- title="Add a reply"
- @click="showReplyForm">Reply...</button>
- </div>
- <div
- v-if="discussion.resolvable"
- class="btn-group"
- role="group">
+ <div class="discussion-with-resolve-btn">
+ <button
+ type="button"
+ class="js-vue-discussion-reply btn btn-text-field mr-sm-2 qa-discussion-reply"
+ title="Add a reply"
+ @click="showReplyForm"
+ >
+ Reply...
+ </button>
+ <div v-if="discussion.resolvable">
<button
type="button"
- class="btn btn-default"
+ class="btn btn-default mr-sm-2"
@click="resolveHandler()"
>
<i
@@ -392,7 +455,7 @@ Please check your network connection and try again.`;
</div>
<div
v-if="discussion.resolvable"
- class="btn-group discussion-actions"
+ class="btn-group discussion-actions ml-sm-2"
role="group"
>
<div
@@ -407,7 +470,7 @@ Please check your network connection and try again.`;
btn-default discussion-create-issue-btn"
data-container="body"
>
- <span v-html="resolveDiscussionsSvg"></span>
+ <icon name="issue-new" />
</a>
</div>
<div
@@ -421,7 +484,7 @@ Please check your network connection and try again.`;
data-container="body"
@click="jumpToNextDiscussion"
>
- <span v-html="nextDiscussionsSvg"></span>
+ <icon name="comment-next" />
</button>
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index f391ed848a4..9ab91e2abe5 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -46,6 +46,7 @@ export default {
'is-requesting being-posted': this.isRequesting,
'disabled-content': this.isDeleting,
target: this.isTarget,
+ 'is-editable': this.note.current_user.can_edit,
};
},
canResolve() {
@@ -173,7 +174,7 @@ export default {
:class="classNameBindings"
:data-award-url="note.toggle_award_path"
:data-note-id="note.id"
- class="note timeline-entry"
+ class="note timeline-entry note-wrapper"
>
<div class="timeline-entry-inner">
<div class="timeline-icon">
@@ -182,7 +183,13 @@ export default {
:img-src="author.avatar_url"
:img-alt="author.name"
:img-size="40"
- />
+ >
+ <slot
+ slot="avatar-badge"
+ name="avatar-badge"
+ >
+ </slot>
+ </user-avatar-link>
</div>
<div class="timeline-content">
<div class="note-header">
@@ -190,6 +197,7 @@ export default {
:author="author"
:created-at="note.created_at"
:note-id="note.id"
+ action-text="commented"
/>
<note-actions
:author-id="author.id"
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index b0faa443a18..69ddfd751e0 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -50,11 +50,19 @@ export default {
},
data() {
return {
+ isFetching: false,
currentFilter: null,
};
},
computed: {
- ...mapGetters(['isNotesFetched', 'discussions', 'getNotesDataByProp', 'discussionCount', 'isLoading']),
+ ...mapGetters([
+ 'isNotesFetched',
+ 'discussions',
+ 'getNotesDataByProp',
+ 'discussionCount',
+ 'isLoading',
+ 'commentsDisabled',
+ ]),
noteableType() {
return this.noteableData.noteableType;
},
@@ -114,6 +122,7 @@ export default {
setTargetNoteHash: 'setTargetNoteHash',
toggleDiscussion: 'toggleDiscussion',
setNotesFetchedState: 'setNotesFetchedState',
+ startTaskList: 'startTaskList',
}),
getComponentName(discussion) {
if (discussion.isSkeletonNote) {
@@ -134,6 +143,10 @@ export default {
return discussion.individual_note ? { note: discussion.notes[0] } : { discussion };
},
fetchNotes() {
+ if (this.isFetching) return null;
+
+ this.isFetching = true;
+
return this.fetchDiscussions({ path: this.getNotesDataByProp('discussionsPath') })
.then(() => {
this.initPolling();
@@ -142,8 +155,10 @@ export default {
this.setLoadingState(false);
this.setNotesFetchedState(true);
eventHub.$emit('fetchedNotesData');
+ this.isFetching = false;
})
.then(() => this.$nextTick())
+ .then(() => this.startTaskList())
.then(() => this.checkLocationHash())
.catch(() => {
this.setLoadingState(false);
@@ -200,6 +215,7 @@ export default {
</ul>
<comment-form
+ v-if="!commentsDisabled"
:noteable-type="noteableType"
:markdown-version="markdownVersion"
/>
diff --git a/app/assets/javascripts/notes/components/toggle_replies_widget.vue b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
new file mode 100644
index 00000000000..78ecbbb9247
--- /dev/null
+++ b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
@@ -0,0 +1,94 @@
+<script>
+import _ from 'underscore';
+import Icon from '~/vue_shared/components/icon.vue';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+
+export default {
+ components: {
+ Icon,
+ UserAvatarLink,
+ TimeAgoTooltip,
+ },
+ props: {
+ collapsed: {
+ type: Boolean,
+ required: true,
+ },
+ replies: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ lastReply() {
+ return this.replies[this.replies.length - 1];
+ },
+ uniqueAuthors() {
+ const authors = this.replies.map(reply => reply.author || {});
+
+ return _.uniq(authors, author => author.username);
+ },
+ className() {
+ return this.collapsed ? 'collapsed' : 'expanded';
+ },
+ },
+ methods: {
+ toggle() {
+ this.$emit('toggle');
+ },
+ },
+};
+</script>
+
+<template>
+ <li
+ :class="className"
+ class="replies-toggle"
+ >
+ <template v-if="collapsed">
+ <icon
+ name="chevron-right"
+ @click.native="toggle"
+ />
+ <div>
+ <user-avatar-link
+ v-for="author in uniqueAuthors"
+ :key="author.username"
+ :link-href="author.path"
+ :img-alt="author.name"
+ :img-src="author.avatar_url"
+ :img-size="26"
+ :tooltip-text="author.name"
+ tooltip-placement="bottom"
+ />
+ </div>
+ <button
+ class="btn btn-link js-replies-text"
+ type="button"
+ @click="toggle"
+ >
+ {{ replies.length }} {{ n__('reply', 'replies', replies.length) }}
+ </button>
+ {{ __('Last reply by') }}
+ <a
+ :href="lastReply.author.path"
+ class="btn btn-link author-link"
+ >
+ {{ lastReply.author.name }}
+ </a>
+ <time-ago-tooltip
+ :time="lastReply.created_at"
+ tooltip-placement="bottom"
+ />
+ </template>
+ <span
+ v-else
+ class="collapse-replies-btn js-collapse-replies"
+ @click="toggle"
+ >
+ <icon name="chevron-down" />
+ {{ s__('Notes|Collapse replies') }}
+ </span>
+ </li>
+</template>
diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js
index 2c3e07c0506..3147dc64c27 100644
--- a/app/assets/javascripts/notes/constants.js
+++ b/app/assets/javascripts/notes/constants.js
@@ -15,6 +15,8 @@ export const MERGE_REQUEST_NOTEABLE_TYPE = 'MergeRequest';
export const UNRESOLVE_NOTE_METHOD_NAME = 'delete';
export const RESOLVE_NOTE_METHOD_NAME = 'post';
export const DESCRIPTION_TYPE = 'changed the description';
+export const HISTORY_ONLY_FILTER_VALUE = 2;
+export const DISCUSSION_FILTERS_DEFAULT_VALUE = 0;
export const NOTEABLE_TYPE_MAPPING = {
Issue: ISSUE_NOTEABLE_TYPE,
diff --git a/app/assets/javascripts/notes/discussion_filters.js b/app/assets/javascripts/notes/discussion_filters.js
index 012ffc4093e..5c5f38a3fb0 100644
--- a/app/assets/javascripts/notes/discussion_filters.js
+++ b/app/assets/javascripts/notes/discussion_filters.js
@@ -1,15 +1,17 @@
import Vue from 'vue';
import DiscussionFilter from './components/discussion_filter.vue';
-export default (store) => {
+export default store => {
const discussionFilterEl = document.getElementById('js-vue-discussion-filter');
if (discussionFilterEl) {
const { defaultFilter, notesFilters } = discussionFilterEl.dataset;
- const defaultValue = defaultFilter ? parseInt(defaultFilter, 10) : null;
+ const selectedValue = defaultFilter ? parseInt(defaultFilter, 10) : null;
const filterValues = notesFilters ? JSON.parse(notesFilters) : {};
- const filters = Object.keys(filterValues).map(entry =>
- ({ title: entry, value: filterValues[entry] }));
+ const filters = Object.keys(filterValues).map(entry => ({
+ title: entry,
+ value: filterValues[entry],
+ }));
return new Vue({
el: discussionFilterEl,
@@ -22,7 +24,7 @@ export default (store) => {
return createElement('discussion-filter', {
props: {
filters,
- defaultValue,
+ selectedValue,
},
});
},
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index b5dd49bc6c9..5b2f0540020 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -1,6 +1,8 @@
+import Vue from 'vue';
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
import Visibility from 'visibilityjs';
+import TaskList from '../../task_list';
import Flash from '../../flash';
import Poll from '../../lib/utils/poll';
import * as types from './mutation_types';
@@ -58,12 +60,13 @@ export const deleteNote = ({ commit, dispatch }, note) =>
dispatch('updateMergeRequestWidget');
});
-export const updateNote = ({ commit }, { endpoint, note }) =>
+export const updateNote = ({ commit, dispatch }, { endpoint, note }) =>
service
.updateNote(endpoint, note)
.then(res => res.json())
.then(res => {
commit(types.UPDATE_NOTE, res);
+ dispatch('startTaskList');
});
export const replyToDiscussion = ({ commit }, { endpoint, data }) =>
@@ -85,6 +88,7 @@ export const createNewNote = ({ commit, dispatch }, { endpoint, data }) =>
commit(types.ADD_NEW_NOTE, res);
dispatch('updateMergeRequestWidget');
+ dispatch('startTaskList');
}
return res;
});
@@ -260,6 +264,8 @@ const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => {
commit(types.ADD_NEW_NOTE, note);
}
});
+
+ dispatch('startTaskList');
}
commit(types.SET_LAST_FETCHED_AT, resp.last_fetched_at);
@@ -335,7 +341,7 @@ export const scrollToNoteIfNeeded = (context, el) => {
};
export const fetchDiscussionDiffLines = ({ commit }, discussion) =>
- axios.get(discussion.truncatedDiffLinesPath).then(({ data }) => {
+ axios.get(discussion.truncated_diff_lines_path).then(({ data }) => {
commit(types.SET_DISCUSSION_DIFF_LINES, {
discussionId: discussion.id,
diffLines: data.truncated_diff_lines,
@@ -364,5 +370,20 @@ export const filterDiscussion = ({ dispatch }, { path, filter }) => {
});
};
+export const setCommentsDisabled = ({ commit }, data) => {
+ commit(types.DISABLE_COMMENTS, data);
+};
+
+export const startTaskList = ({ dispatch }) =>
+ Vue.nextTick(
+ () =>
+ new TaskList({
+ dataType: 'note',
+ fieldName: 'note',
+ selector: '.notes .is-editable',
+ onSuccess: () => dispatch('startTaskList'),
+ }),
+ );
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/notes/stores/collapse_utils.js b/app/assets/javascripts/notes/stores/collapse_utils.js
index 4532226aa07..bee6d4f0329 100644
--- a/app/assets/javascripts/notes/stores/collapse_utils.js
+++ b/app/assets/javascripts/notes/stores/collapse_utils.js
@@ -70,7 +70,7 @@ export const collapseSystemNotes = notes => {
} else if (lastDescriptionSystemNote) {
const timeDifferenceMinutes = getTimeDifferenceMinutes(lastDescriptionSystemNote, note);
- // are they less than 10 minutes appart?
+ // are they less than 10 minutes apart?
if (timeDifferenceMinutes > 10) {
// reset counter
descriptionChangedTimes = 1;
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index e4f36154fcd..8df95c279eb 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -192,5 +192,7 @@ export const firstUnresolvedDiscussionId = (state, getters) => diffOrder => {
return getters.unresolvedDiscussionsIdsByDate[0];
};
+export const commentsDisabled = state => state.commentsDisabled;
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js
index 400142668ea..8aea269ea7d 100644
--- a/app/assets/javascripts/notes/stores/modules/index.js
+++ b/app/assets/javascripts/notes/stores/modules/index.js
@@ -21,6 +21,7 @@ export default () => ({
noteableData: {
current_user: {},
},
+ commentsDisabled: false,
},
actions,
getters,
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index 2fa53aef1d4..dfbf3b7b34b 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -15,6 +15,7 @@ export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION';
export const SET_DISCUSSION_DIFF_LINES = 'SET_DISCUSSION_DIFF_LINES';
export const SET_NOTES_FETCHED_STATE = 'SET_NOTES_FETCHED_STATE';
export const SET_NOTES_LOADING_STATE = 'SET_NOTES_LOADING_STATE';
+export const DISABLE_COMMENTS = 'DISABLE_COMMENTS';
// DISCUSSION
export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index 65085452139..f6054e0be87 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -102,7 +102,7 @@ export default {
discussionsData.forEach(discussion => {
if (discussion.diff_file) {
Object.assign(discussion, {
- fileHash: discussion.diff_file.file_hash,
+ file_hash: discussion.diff_file.file_hash,
truncated_diff_lines: discussion.truncated_diff_lines || [],
});
}
@@ -195,7 +195,7 @@ export default {
const selectedDiscussion = state.discussions.find(disc => disc.id === note.id);
note.expanded = true; // override expand flag to prevent collapse
if (note.diff_file) {
- Object.assign(note, { fileHash: note.diff_file.file_hash });
+ Object.assign(note, { file_hash: note.diff_file.file_hash });
}
Object.assign(selectedDiscussion, { ...note });
},
@@ -225,4 +225,8 @@ export default {
discussion.truncated_diff_lines = diffLines;
},
+
+ [types.DISABLE_COMMENTS](state, value) {
+ state.commentsDisabled = value;
+ },
};
diff --git a/app/assets/javascripts/pages/groups/clusters/destroy/index.js b/app/assets/javascripts/pages/groups/clusters/destroy/index.js
new file mode 100644
index 00000000000..8001d2dd1da
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/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/groups/clusters/index/index.js b/app/assets/javascripts/pages/groups/clusters/index/index.js
new file mode 100644
index 00000000000..845a5f7042c
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/clusters/index/index.js
@@ -0,0 +1,5 @@
+import initDismissableCallout from '~/dismissable_callout';
+
+document.addEventListener('DOMContentLoaded', () => {
+ initDismissableCallout('.gcp-signup-offer');
+});
diff --git a/app/assets/javascripts/pages/groups/clusters/show/index.js b/app/assets/javascripts/pages/groups/clusters/show/index.js
new file mode 100644
index 00000000000..8001d2dd1da
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/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/clusters/update/index.js b/app/assets/javascripts/pages/groups/clusters/update/index.js
new file mode 100644
index 00000000000..8001d2dd1da
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/clusters/update/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/index.js b/app/assets/javascripts/pages/groups/index.js
new file mode 100644
index 00000000000..bf80d8b8193
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/index.js
@@ -0,0 +1,16 @@
+import initDismissableCallout from '~/dismissable_callout';
+import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const { page } = document.body.dataset;
+ const newClusterViews = [
+ 'groups:clusters:new',
+ 'groups:clusters:create_gcp',
+ 'groups:clusters:create_user',
+ ];
+
+ if (newClusterViews.indexOf(page) > -1) {
+ initDismissableCallout('.gcp-signup-offer');
+ initGkeDropdowns();
+ }
+});
diff --git a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
index 2c683a39f42..9d19e4a095d 100644
--- a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
+++ b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
@@ -1,54 +1,66 @@
<script>
- import axios from '~/lib/utils/axios_utils';
- import createFlash from '~/flash';
- import GlModal from '~/vue_shared/components/gl_modal.vue';
- import { s__, sprintf } from '~/locale';
- import { visitUrl } from '~/lib/utils/url_utility';
- import eventHub from '../event_hub';
+import axios from '~/lib/utils/axios_utils';
+import createFlash from '~/flash';
+import GlModal from '~/vue_shared/components/gl_modal.vue';
+import { s__, sprintf } from '~/locale';
+import { visitUrl } from '~/lib/utils/url_utility';
+import eventHub from '../event_hub';
- export default {
- components: {
- GlModal,
+export default {
+ components: {
+ GlModal,
+ },
+ props: {
+ milestoneTitle: {
+ type: String,
+ required: true,
},
- props: {
- milestoneTitle: {
- type: String,
- required: true,
- },
- url: {
- type: String,
- required: true,
- },
- groupName: {
- type: String,
- required: true,
- },
+ url: {
+ type: String,
+ required: true,
},
- computed: {
- title() {
- return sprintf(s__('Milestones|Promote %{milestoneTitle} to group milestone?'), { milestoneTitle: this.milestoneTitle });
- },
- text() {
- return sprintf(s__(`Milestones|Promoting %{milestoneTitle} will make it available for all projects inside %{groupName}.
+ groupName: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ title() {
+ return sprintf(s__('Milestones|Promote %{milestoneTitle} to group milestone?'), {
+ milestoneTitle: this.milestoneTitle,
+ });
+ },
+ text() {
+ return sprintf(
+ s__(`Milestones|Promoting %{milestoneTitle} will make it available for all projects inside %{groupName}.
Existing project milestones with the same title will be merged.
- This action cannot be reversed.`), { milestoneTitle: this.milestoneTitle, groupName: this.groupName });
- },
+ This action cannot be reversed.`),
+ { milestoneTitle: this.milestoneTitle, groupName: this.groupName },
+ );
},
- methods: {
- onSubmit() {
- eventHub.$emit('promoteMilestoneModal.requestStarted', this.url);
- return axios.post(this.url, { params: { format: 'json' } })
- .then((response) => {
- eventHub.$emit('promoteMilestoneModal.requestFinished', { milestoneUrl: this.url, successful: true });
- visitUrl(response.data.url);
- })
- .catch((error) => {
- eventHub.$emit('promoteMilestoneModal.requestFinished', { milestoneUrl: this.url, successful: false });
- createFlash(error);
+ },
+ methods: {
+ onSubmit() {
+ eventHub.$emit('promoteMilestoneModal.requestStarted', this.url);
+ return axios
+ .post(this.url, { params: { format: 'json' } })
+ .then(response => {
+ eventHub.$emit('promoteMilestoneModal.requestFinished', {
+ milestoneUrl: this.url,
+ successful: true,
});
- },
+ visitUrl(response.data.url);
+ })
+ .catch(error => {
+ eventHub.$emit('promoteMilestoneModal.requestFinished', {
+ milestoneUrl: this.url,
+ successful: false,
+ });
+ createFlash(error);
+ });
},
- };
+ },
+};
</script>
<template>
<gl-modal
@@ -65,4 +77,3 @@
{{ text }}
</gl-modal>
</template>
-
diff --git a/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js b/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js
deleted file mode 100644
index d4f34e32a48..00000000000
--- a/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
-
-document.addEventListener('DOMContentLoaded', () => {
- initGkeDropdowns();
-});
diff --git a/app/assets/javascripts/pages/projects/clusters/index/index.js b/app/assets/javascripts/pages/projects/clusters/index/index.js
index e4b8baede58..845a5f7042c 100644
--- a/app/assets/javascripts/pages/projects/clusters/index/index.js
+++ b/app/assets/javascripts/pages/projects/clusters/index/index.js
@@ -1,5 +1,5 @@
-import ClustersIndex from '~/clusters/clusters_index';
+import initDismissableCallout from '~/dismissable_callout';
document.addEventListener('DOMContentLoaded', () => {
- new ClustersIndex(); // eslint-disable-line no-new
+ initDismissableCallout('.gcp-signup-offer');
});
diff --git a/app/assets/javascripts/pages/projects/jobs/index/index.js b/app/assets/javascripts/pages/projects/jobs/index/index.js
new file mode 100644
index 00000000000..1b57c67f16b
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/jobs/index/index.js
@@ -0,0 +1,16 @@
+import Vue from 'vue';
+import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const remainingTimeElements = document.querySelectorAll('.js-remaining-time');
+ remainingTimeElements.forEach(
+ el =>
+ new Vue({
+ ...GlCountdown,
+ el,
+ propsData: {
+ endDateString: el.dateTime,
+ },
+ }),
+ );
+});
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index 52d66beefc9..a6bee49a6b1 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -64,7 +64,9 @@ export default class Project {
const projectId = $(this).data('project-id');
const cookieKey = `hide_auto_devops_implicitly_enabled_banner_${projectId}`;
Cookies.set(cookieKey, 'false');
- $(this).parents('.auto-devops-implicitly-enabled-banner').remove();
+ $(this)
+ .parents('.auto-devops-implicitly-enabled-banner')
+ .remove();
return e.preventDefault();
});
Project.projectSelectDropdown();
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index a16f7e6b77c..c0ec7a5dc94 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -1,202 +1,200 @@
<script>
- import projectFeatureSetting from './project_feature_setting.vue';
- import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue';
- import projectSettingRow from './project_setting_row.vue';
- import { visibilityOptions, visibilityLevelDescriptions } from '../constants';
- import { toggleHiddenClassBySelector } from '../external';
+import projectFeatureSetting from './project_feature_setting.vue';
+import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue';
+import projectSettingRow from './project_setting_row.vue';
+import { visibilityOptions, visibilityLevelDescriptions } from '../constants';
+import { toggleHiddenClassBySelector } from '../external';
- export default {
- components: {
- projectFeatureSetting,
- projectFeatureToggle,
- projectSettingRow,
- },
+export default {
+ components: {
+ projectFeatureSetting,
+ projectFeatureToggle,
+ projectSettingRow,
+ },
- props: {
- currentSettings: {
- type: Object,
- required: true,
- },
- canChangeVisibilityLevel: {
- type: Boolean,
- required: false,
- default: false,
- },
- allowedVisibilityOptions: {
- type: Array,
- required: false,
- default: () => [0, 10, 20],
- },
- lfsAvailable: {
- type: Boolean,
- required: false,
- default: false,
- },
- registryAvailable: {
- type: Boolean,
- required: false,
- default: false,
- },
- visibilityHelpPath: {
- type: String,
- required: false,
- default: '',
- },
- lfsHelpPath: {
- type: String,
- required: false,
- default: '',
- },
- registryHelpPath: {
- type: String,
- required: false,
- default: '',
- },
- pagesAvailable: {
- type: Boolean,
- required: false,
- default: false,
- },
- pagesAccessControlEnabled: {
- type: Boolean,
- required: false,
- default: false,
- },
- pagesHelpPath: {
- type: String,
- required: false,
- default: '',
- },
+ props: {
+ currentSettings: {
+ type: Object,
+ required: true,
+ },
+ canChangeVisibilityLevel: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ allowedVisibilityOptions: {
+ type: Array,
+ required: false,
+ default: () => [0, 10, 20],
+ },
+ lfsAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ registryAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ visibilityHelpPath: {
+ type: String,
+ required: false,
+ default: '',
},
+ lfsHelpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ registryHelpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ pagesAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ pagesAccessControlEnabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ pagesHelpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
- data() {
- const defaults = {
- visibilityOptions,
- visibilityLevel: visibilityOptions.PUBLIC,
- issuesAccessLevel: 20,
- repositoryAccessLevel: 20,
- mergeRequestsAccessLevel: 20,
- buildsAccessLevel: 20,
- wikiAccessLevel: 20,
- snippetsAccessLevel: 20,
- pagesAccessLevel: 20,
- containerRegistryEnabled: true,
- lfsEnabled: true,
- requestAccessEnabled: true,
- highlightChangesClass: false,
- };
+ data() {
+ const defaults = {
+ visibilityOptions,
+ visibilityLevel: visibilityOptions.PUBLIC,
+ issuesAccessLevel: 20,
+ repositoryAccessLevel: 20,
+ mergeRequestsAccessLevel: 20,
+ buildsAccessLevel: 20,
+ wikiAccessLevel: 20,
+ snippetsAccessLevel: 20,
+ pagesAccessLevel: 20,
+ containerRegistryEnabled: true,
+ lfsEnabled: true,
+ requestAccessEnabled: true,
+ highlightChangesClass: false,
+ };
- return { ...defaults, ...this.currentSettings };
- },
+ return { ...defaults, ...this.currentSettings };
+ },
- computed: {
- featureAccessLevelOptions() {
- const options = [
- [10, 'Only Project Members'],
- ];
- if (this.visibilityLevel !== visibilityOptions.PRIVATE) {
- options.push([20, 'Everyone With Access']);
- }
- return options;
- },
+ computed: {
+ featureAccessLevelOptions() {
+ const options = [[10, 'Only Project Members']];
+ if (this.visibilityLevel !== visibilityOptions.PRIVATE) {
+ options.push([20, 'Everyone With Access']);
+ }
+ return options;
+ },
- repoFeatureAccessLevelOptions() {
- return this.featureAccessLevelOptions.filter(
- ([value]) => value <= this.repositoryAccessLevel,
- );
- },
+ repoFeatureAccessLevelOptions() {
+ return this.featureAccessLevelOptions.filter(
+ ([value]) => value <= this.repositoryAccessLevel,
+ );
+ },
- pagesFeatureAccessLevelOptions() {
- if (this.visibilityLevel !== visibilityOptions.PUBLIC) {
- return this.featureAccessLevelOptions.concat([[30, 'Everyone']]);
- }
- return this.featureAccessLevelOptions;
- },
+ pagesFeatureAccessLevelOptions() {
+ if (this.visibilityLevel !== visibilityOptions.PUBLIC) {
+ return this.featureAccessLevelOptions.concat([[30, 'Everyone']]);
+ }
+ return this.featureAccessLevelOptions;
+ },
- repositoryEnabled() {
- return this.repositoryAccessLevel > 0;
- },
+ repositoryEnabled() {
+ return this.repositoryAccessLevel > 0;
+ },
- visibilityLevelDescription() {
- return visibilityLevelDescriptions[this.visibilityLevel];
- },
+ visibilityLevelDescription() {
+ return visibilityLevelDescriptions[this.visibilityLevel];
},
+ },
- watch: {
- visibilityLevel(value, oldValue) {
- if (value === visibilityOptions.PRIVATE) {
- // when private, features are restricted to "only team members"
- this.issuesAccessLevel = Math.min(10, this.issuesAccessLevel);
- this.repositoryAccessLevel = Math.min(10, this.repositoryAccessLevel);
- this.mergeRequestsAccessLevel = Math.min(10, this.mergeRequestsAccessLevel);
- this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel);
- this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel);
- this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel);
- if (this.pagesAccessLevel === 20) {
- // When from Internal->Private narrow access for only members
- this.pagesAccessLevel = 10;
- }
- this.highlightChanges();
- } else if (oldValue === visibilityOptions.PRIVATE) {
- // if changing away from private, make enabled features more permissive
- if (this.issuesAccessLevel > 0) this.issuesAccessLevel = 20;
- if (this.repositoryAccessLevel > 0) this.repositoryAccessLevel = 20;
- if (this.mergeRequestsAccessLevel > 0) this.mergeRequestsAccessLevel = 20;
- if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20;
- if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20;
- if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20;
- if (this.pagesAccessLevel === 10) this.pagesAccessLevel = 20;
- this.highlightChanges();
+ watch: {
+ visibilityLevel(value, oldValue) {
+ if (value === visibilityOptions.PRIVATE) {
+ // when private, features are restricted to "only team members"
+ this.issuesAccessLevel = Math.min(10, this.issuesAccessLevel);
+ this.repositoryAccessLevel = Math.min(10, this.repositoryAccessLevel);
+ this.mergeRequestsAccessLevel = Math.min(10, this.mergeRequestsAccessLevel);
+ this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel);
+ this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel);
+ this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel);
+ if (this.pagesAccessLevel === 20) {
+ // When from Internal->Private narrow access for only members
+ this.pagesAccessLevel = 10;
}
- },
+ this.highlightChanges();
+ } else if (oldValue === visibilityOptions.PRIVATE) {
+ // if changing away from private, make enabled features more permissive
+ if (this.issuesAccessLevel > 0) this.issuesAccessLevel = 20;
+ if (this.repositoryAccessLevel > 0) this.repositoryAccessLevel = 20;
+ if (this.mergeRequestsAccessLevel > 0) this.mergeRequestsAccessLevel = 20;
+ if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20;
+ if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20;
+ if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20;
+ if (this.pagesAccessLevel === 10) this.pagesAccessLevel = 20;
+ this.highlightChanges();
+ }
+ },
- repositoryAccessLevel(value, oldValue) {
- if (value < oldValue) {
- // sub-features cannot have more premissive access level
- this.mergeRequestsAccessLevel = Math.min(this.mergeRequestsAccessLevel, value);
- this.buildsAccessLevel = Math.min(this.buildsAccessLevel, value);
+ repositoryAccessLevel(value, oldValue) {
+ if (value < oldValue) {
+ // sub-features cannot have more premissive access level
+ this.mergeRequestsAccessLevel = Math.min(this.mergeRequestsAccessLevel, value);
+ this.buildsAccessLevel = Math.min(this.buildsAccessLevel, value);
- if (value === 0) {
- this.containerRegistryEnabled = false;
- this.lfsEnabled = false;
- }
- } else if (oldValue === 0) {
- this.mergeRequestsAccessLevel = value;
- this.buildsAccessLevel = value;
- this.containerRegistryEnabled = true;
- this.lfsEnabled = true;
+ if (value === 0) {
+ this.containerRegistryEnabled = false;
+ this.lfsEnabled = false;
}
- },
+ } else if (oldValue === 0) {
+ this.mergeRequestsAccessLevel = value;
+ this.buildsAccessLevel = value;
+ this.containerRegistryEnabled = true;
+ this.lfsEnabled = true;
+ }
+ },
- issuesAccessLevel(value, oldValue) {
- if (value === 0) toggleHiddenClassBySelector('.issues-feature', true);
- else if (oldValue === 0) toggleHiddenClassBySelector('.issues-feature', false);
- },
+ issuesAccessLevel(value, oldValue) {
+ if (value === 0) toggleHiddenClassBySelector('.issues-feature', true);
+ else if (oldValue === 0) toggleHiddenClassBySelector('.issues-feature', false);
+ },
- mergeRequestsAccessLevel(value, oldValue) {
- if (value === 0) toggleHiddenClassBySelector('.merge-requests-feature', true);
- else if (oldValue === 0) toggleHiddenClassBySelector('.merge-requests-feature', false);
- },
+ mergeRequestsAccessLevel(value, oldValue) {
+ if (value === 0) toggleHiddenClassBySelector('.merge-requests-feature', true);
+ else if (oldValue === 0) toggleHiddenClassBySelector('.merge-requests-feature', false);
+ },
- buildsAccessLevel(value, oldValue) {
- if (value === 0) toggleHiddenClassBySelector('.builds-feature', true);
- else if (oldValue === 0) toggleHiddenClassBySelector('.builds-feature', false);
- },
+ buildsAccessLevel(value, oldValue) {
+ if (value === 0) toggleHiddenClassBySelector('.builds-feature', true);
+ else if (oldValue === 0) toggleHiddenClassBySelector('.builds-feature', false);
},
+ },
- methods: {
- highlightChanges() {
- this.highlightChangesClass = true;
- this.$nextTick(() => {
- this.highlightChangesClass = false;
- });
- },
+ methods: {
+ highlightChanges() {
+ this.highlightChangesClass = true;
+ this.$nextTick(() => {
+ this.highlightChangesClass = false;
+ });
+ },
- visibilityAllowed(option) {
- return this.allowedVisibilityOptions.includes(option);
- },
+ visibilityAllowed(option) {
+ return this.allowedVisibilityOptions.includes(option);
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue
index 75cb6374ad5..f970a5ebb64 100644
--- a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue
+++ b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue
@@ -1,8 +1,15 @@
<script>
import _ from 'underscore';
import { s__, sprintf } from '~/locale';
+import { GlModal, GlModalDirective } from '@gitlab-org/gitlab-ui';
export default {
+ components: {
+ GlModal,
+ },
+ directives: {
+ 'gl-modal': GlModalDirective,
+ },
props: {
deleteWikiUrl: {
type: String,
@@ -54,7 +61,7 @@ export default {
>
{{ __('Delete') }}
</button>
- <gl-ui-modal
+ <gl-modal
:title="title"
:ok-title="s__('WikiPageConfirmDelete|Delete page')"
:modal-id="modalId"
@@ -81,6 +88,6 @@ export default {
name="authenticity_token"
/>
</form>
- </gl-ui-modal>
+ </gl-modal>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/empty_state.vue b/app/assets/javascripts/pipelines/components/empty_state.vue
index c5a45afc634..8a0259ed5a5 100644
--- a/app/assets/javascripts/pipelines/components/empty_state.vue
+++ b/app/assets/javascripts/pipelines/components/empty_state.vue
@@ -1,6 +1,11 @@
<script>
+import { GlButton } from '@gitlab-org/gitlab-ui';
+
export default {
name: 'PipelinesEmptyState',
+ components: {
+ GlButton,
+ },
props: {
helpPagePath: {
type: String,
@@ -41,12 +46,13 @@ export default {
</p>
<div class="text-center">
- <a
+ <gl-button
:href="helpPagePath"
- class="btn btn-primary js-get-started-pipelines"
+ variant="primary"
+ class="js-get-started-pipelines"
>
{{ s__('Pipelines|Get started with Pipelines') }}
- </a>
+ </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index b82e28a0735..f6a97236ebf 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -1,10 +1,9 @@
<script>
-import $ from 'jquery';
+import { GlTooltipDirective, GlButton } from '@gitlab-org/gitlab-ui';
import axios from '~/lib/utils/axios_utils';
import { dasherize } from '~/lib/utils/text_utility';
import { __ } from '~/locale';
import createFlash from '~/flash';
-import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
/**
@@ -20,23 +19,20 @@ import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
Icon,
+ GlButton,
},
-
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
-
props: {
tooltipText: {
type: String,
required: true,
},
-
link: {
type: String,
required: true,
},
-
actionIcon: {
type: String,
required: true,
@@ -47,7 +43,6 @@ export default {
isDisabled: false,
};
},
-
computed: {
cssClass() {
const actionIconDash = dasherize(this.actionIcon);
@@ -62,8 +57,7 @@ export default {
*
*/
onClickAction() {
- $(this.$el).tooltip('hide');
-
+ this.$root.$emit('bv::hide::tooltip', `js-ci-action-${this.link}`);
this.isDisabled = true;
axios
@@ -82,18 +76,16 @@ export default {
};
</script>
<template>
- <button
- v-tooltip
+ <gl-button
+ :id="`js-ci-action-${link}`"
+ v-gl-tooltip="{ boundary: 'viewport' }"
:title="tooltipText"
:class="cssClass"
:disabled="isDisabled"
- type="button"
class="js-ci-action btn btn-blank
btn-transparent ci-action-icon-container ci-action-icon-wrapper"
- data-container="body"
- data-boundary="viewport"
@click="onClickAction"
>
<icon :name="actionIcon"/>
- </button>
+ </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 23c0be7742e..4de8b3401e8 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -1,10 +1,12 @@
<script>
import _ from 'underscore';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import StageColumnComponent from './stage_column_component.vue';
export default {
components: {
StageColumnComponent,
+ GlLoadingIcon,
},
props: {
isLoading: {
diff --git a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
index 34bada533df..2c3cb1959b5 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
@@ -1,8 +1,8 @@
<script>
import $ from 'jquery';
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import JobItem from './job_item.vue';
-import tooltip from '../../../vue_shared/directives/tooltip';
/**
* Renders the dropdown for the pipeline graph.
@@ -12,32 +12,27 @@ import tooltip from '../../../vue_shared/directives/tooltip';
*/
export default {
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
-
components: {
JobItem,
CiIcon,
},
-
props: {
group: {
type: Object,
required: true,
},
},
-
computed: {
tooltipText() {
const { name, status } = this.group;
return `${name} - ${status.label}`;
},
},
-
mounted() {
this.stopDropdownClickPropagation();
},
-
methods: {
/**
* When the user right clicks or cmd/ctrl + click in the group name or the action icon
@@ -65,12 +60,10 @@ export default {
<template>
<div class="ci-job-dropdown-container dropdown dropright">
<button
- v-tooltip
+ v-gl-tooltip.hover="{ boundary: 'viewport' }"
:title="tooltipText"
type="button"
data-toggle="dropdown"
- data-container="body"
- data-boundary="viewport"
data-display="static"
class="dropdown-menu-toggle build-content"
>
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue
index a1504592bbc..182849c6455 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue
@@ -1,7 +1,9 @@
<script>
import ActionComponent from './action_component.vue';
import JobNameComponent from './job_name_component.vue';
-import tooltip from '../../../vue_shared/directives/tooltip';
+import { GlTooltipDirective, GlLink } from '@gitlab-org/gitlab-ui';
+import { sprintf } from '~/locale';
+import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
/**
* Renders the badge for the pipeline graph and the job's dropdown.
@@ -32,10 +34,12 @@ export default {
components: {
ActionComponent,
JobNameComponent,
+ GlLink,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
+ mixins: [delayedJobMixin],
props: {
job: {
type: Object,
@@ -59,17 +63,23 @@ export default {
tooltipText() {
const textBuilder = [];
+ const { name: jobName } = this.job;
- if (this.job.name) {
- textBuilder.push(this.job.name);
+ if (jobName) {
+ textBuilder.push(jobName);
}
- if (this.job.name && this.status.tooltip) {
+ const { tooltip: statusTooltip } = this.status;
+ if (jobName && statusTooltip) {
textBuilder.push('-');
}
- if (this.status.tooltip) {
- textBuilder.push(this.job.status.tooltip);
+ if (statusTooltip) {
+ if (this.isDelayedJob) {
+ textBuilder.push(sprintf(statusTooltip, { remainingTime: this.remainingTime }));
+ } else {
+ textBuilder.push(statusTooltip);
+ }
}
return textBuilder.join(' ');
@@ -78,7 +88,6 @@ export default {
tooltipBoundary() {
return this.dropdownLength < 5 ? 'viewport' : null;
},
-
/**
* Verifies if the provided job has an action path
*
@@ -97,30 +106,26 @@ export default {
</script>
<template>
<div class="ci-job-component">
- <a
+ <gl-link
v-if="status.has_details"
- v-tooltip
+ v-gl-tooltip="{ boundary: tooltipBoundary }"
:href="status.details_path"
:title="tooltipText"
:class="cssClassJobName"
- :data-boundary="tooltipBoundary"
- data-container="body"
class="js-pipeline-graph-job-link"
>
-
<job-name-component
:name="job.name"
:status="job.status"
/>
- </a>
+ </gl-link>
<div
v-else
- v-tooltip
+ v-gl-tooltip
:title="tooltipText"
:class="cssClassJobName"
class="js-job-component-tooltip non-details-job-component"
- data-container="body"
>
<job-name-component
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 efbab51d200..d5f931943d5 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -13,34 +13,28 @@ export default {
type: String,
required: true,
},
-
groups: {
type: Array,
required: true,
},
-
isFirstColumn: {
type: Boolean,
required: false,
default: false,
},
-
stageConnectorClass: {
type: String,
required: false,
default: '',
},
},
-
methods: {
groupId(group) {
return `ci-badge-${_.escape(group.name)}`;
},
-
buildConnnectorClass(index) {
return index === 0 && !this.isFirstColumn ? 'left-connector' : '';
},
-
pipelineActionRequestComplete() {
this.$emit('refreshPipelineGraph');
},
@@ -50,7 +44,8 @@ export default {
<template>
<li
:class="stageConnectorClass"
- class="stage-column">
+ class="stage-column"
+ >
<div class="stage-name">
{{ title }}
</div>
@@ -78,7 +73,6 @@ export default {
:group="group"
@pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
-
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue
index 1f9187c3d65..8f004b491c8 100644
--- a/app/assets/javascripts/pipelines/components/header_component.vue
+++ b/app/assets/javascripts/pipelines/components/header_component.vue
@@ -1,4 +1,5 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import ciHeader from '../../vue_shared/components/header_ci_component.vue';
import eventHub from '../event_hub';
@@ -6,6 +7,7 @@ export default {
name: 'PipelineHeaderSection',
components: {
ciHeader,
+ GlLoadingIcon,
},
props: {
pipeline: {
diff --git a/app/assets/javascripts/pipelines/components/nav_controls.vue b/app/assets/javascripts/pipelines/components/nav_controls.vue
index efb80d3a3c0..0911acbb131 100644
--- a/app/assets/javascripts/pipelines/components/nav_controls.vue
+++ b/app/assets/javascripts/pipelines/components/nav_controls.vue
@@ -1,10 +1,13 @@
<script>
+import { GlLink, GlButton } from '@gitlab-org/gitlab-ui';
import LoadingButton from '../../vue_shared/components/loading_button.vue';
export default {
name: 'PipelineNavControls',
components: {
LoadingButton,
+ GlLink,
+ GlButton,
},
props: {
newPipelinePath: {
@@ -40,28 +43,29 @@ export default {
</script>
<template>
<div class="nav-controls">
- <a
+ <gl-button
v-if="newPipelinePath"
:href="newPipelinePath"
- class="btn btn-success js-run-pipeline"
+ variant="success"
+ class="js-run-pipeline"
>
{{ s__('Pipelines|Run Pipeline') }}
- </a>
+ </gl-button>
<loading-button
v-if="resetCachePath"
:loading="isResetCacheButtonLoading"
:label="s__('Pipelines|Clear Runner Caches')"
- class="btn btn-default js-clear-cache"
+ class="js-clear-cache"
@click="onClickResetCache"
/>
- <a
+ <gl-button
v-if="ciLintPath"
:href="ciLintPath"
- class="btn btn-default js-ci-lint"
+ class="js-ci-lint"
>
{{ s__('Pipelines|CI Lint') }}
- </a>
+ </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue
index 40df07650c9..be4b37f3c8c 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue
@@ -1,14 +1,15 @@
<script>
-import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
-import tooltip from '../../vue_shared/directives/tooltip';
-import popover from '../../vue_shared/directives/popover';
+import { GlLink, GlTooltipDirective } from '@gitlab-org/gitlab-ui';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import popover from '~/vue_shared/directives/popover';
export default {
components: {
- userAvatarLink,
+ UserAvatarLink,
+ GlLink,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
popover,
},
props: {
@@ -47,11 +48,12 @@ export default {
</script>
<template>
<div class="table-section section-15 d-none d-sm-none d-md-block pipeline-tags">
- <a
+ <gl-link
:href="pipeline.path"
- class="js-pipeline-url-link">
+ class="js-pipeline-url-link"
+ >
<span class="pipeline-id">#{{ pipeline.id }}</span>
- </a>
+ </gl-link>
<span>by</span>
<user-avatar-link
v-if="user"
@@ -68,36 +70,41 @@ export default {
<div class="label-container">
<span
v-if="pipeline.flags.latest"
- v-tooltip
+ v-gl-tooltip
class="js-pipeline-url-latest badge badge-success"
- title="Latest pipeline for this branch">
+ title="Latest pipeline for this branch"
+ >
latest
</span>
<span
v-if="pipeline.flags.yaml_errors"
- v-tooltip
+ v-gl-tooltip
:title="pipeline.yaml_errors"
- class="js-pipeline-url-yaml badge badge-danger">
+ class="js-pipeline-url-yaml badge badge-danger"
+ >
yaml invalid
</span>
<span
v-if="pipeline.flags.failure_reason"
- v-tooltip
+ v-gl-tooltip
:title="pipeline.failure_reason"
- class="js-pipeline-url-failure badge badge-danger">
+ class="js-pipeline-url-failure badge badge-danger"
+ >
error
</span>
- <a
+ <gl-link
v-if="pipeline.flags.auto_devops"
v-popover="popoverOptions"
tabindex="0"
class="js-pipeline-url-autodevops badge badge-info autodevops-badge"
- role="button">
+ role="button"
+ >
Auto DevOps
- </a>
+ </gl-link>
<span
v-if="pipeline.flags.stuck"
- class="js-pipeline-url-stuck badge badge-warning">
+ class="js-pipeline-url-stuck badge badge-warning"
+ >
stuck
</span>
</div>
diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue
index ea526cf1309..fcd8a54c9c1 100644
--- a/app/assets/javascripts/pipelines/components/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines.vue
@@ -155,14 +155,6 @@ export default {
);
},
- shouldRenderPagination() {
- return (
- !this.isLoading &&
- this.state.pipelines.length &&
- this.state.pageInfo.total > this.state.pageInfo.perPage
- );
- },
-
emptyTabMessage() {
const { scopes } = this.$options;
const possibleScopes = [scopes.pending, scopes.running, scopes.finished];
@@ -232,36 +224,6 @@ export default {
this.setCommonData(resp.data.pipelines);
}
},
- /**
- * Handles URL and query parameter changes.
- * When the user uses the pagination or the tabs,
- * - update URL
- * - Make API request to the server with new parameters
- * - Update the polling function
- * - Update the internal state
- */
- updateContent(parameters) {
- this.updateInternalState(parameters);
-
- // fetch new data
- return this.service
- .getPipelines(this.requestData)
- .then(response => {
- this.isLoading = false;
- this.successCallback(response);
-
- // restart polling
- this.poll.restart({ data: this.requestData });
- })
- .catch(() => {
- this.isLoading = false;
- this.errorCallback();
-
- // restart polling
- this.poll.restart({ data: this.requestData });
- });
- },
-
handleResetRunnersCache(endpoint) {
this.isResetCacheButtonLoading = true;
diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
index d032ad73f71..811495c45a9 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
@@ -1,17 +1,19 @@
<script>
+import { GlButton, GlTooltipDirective, GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import { s__, sprintf } from '~/locale';
-import eventHub from '../event_hub';
-import icon from '../../vue_shared/components/icon.vue';
-import tooltip from '../../vue_shared/directives/tooltip';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
+import eventHub from '../event_hub';
+import Icon from '../../vue_shared/components/icon.vue';
export default {
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
components: {
- icon,
+ Icon,
GlCountdown,
+ GlButton,
+ GlLoadingIcon,
},
props: {
actions: {
@@ -29,7 +31,7 @@ export default {
if (action.scheduled_at) {
const confirmationMessage = sprintf(
s__(
- "DelayedJobs|Are you sure you want to run %{jobName} immediately? This job will run automatically after it's timer finishes.",
+ "DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after it's timer finishes.",
),
{ jobName: action.name },
);
@@ -57,14 +59,12 @@ export default {
</script>
<template>
<div class="btn-group">
- <button
- v-tooltip
+ <gl-button
+ v-gl-tooltip
:disabled="isLoading"
- type="button"
class="dropdown-new btn btn-default js-pipeline-dropdown-manual-actions"
title="Manual job"
data-toggle="dropdown"
- data-placement="top"
aria-label="Manual job"
>
<icon
@@ -76,17 +76,16 @@ export default {
aria-hidden="true">
</i>
<gl-loading-icon v-if="isLoading" />
- </button>
+ </gl-button>
<ul class="dropdown-menu dropdown-menu-right">
<li
v-for="action in actions"
:key="action.path"
>
- <button
+ <gl-button
:class="{ disabled: isActionDisabled(action) }"
:disabled="isActionDisabled(action)"
- type="button"
class="js-pipeline-action-link no-btn btn"
@click="onClickAction(action)"
>
@@ -98,7 +97,7 @@ export default {
<icon name="clock" />
<gl-countdown :end-date-string="action.scheduled_at" />
</span>
- </button>
+ </gl-button>
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
index d40de95e051..2abb24b87b6 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
@@ -1,13 +1,15 @@
<script>
-import tooltip from '../../vue_shared/directives/tooltip';
-import icon from '../../vue_shared/components/icon.vue';
+import { GlLink, GlButton, GlTooltipDirective } from '@gitlab-org/gitlab-ui';
+import Icon from '~/vue_shared/components/icon.vue';
export default {
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
components: {
- icon,
+ Icon,
+ GlLink,
+ GlButton,
},
props: {
artifacts: {
@@ -22,11 +24,10 @@ export default {
class="btn-group"
role="group"
>
- <button
- v-tooltip
- class="dropdown-toggle btn btn-default build-artifacts js-pipeline-dropdown-download"
+ <gl-button
+ v-gl-tooltip
+ class="dropdown-toggle build-artifacts js-pipeline-dropdown-download"
title="Artifacts"
- data-placement="top"
data-toggle="dropdown"
aria-label="Artifacts"
>
@@ -36,18 +37,19 @@ export default {
aria-hidden="true"
>
</i>
- </button>
+ </gl-button>
<ul class="dropdown-menu dropdown-menu-right">
<li
v-for="(artifact, i) in artifacts"
- :key="i">
- <a
+ :key="i"
+ >
+ <gl-link
:href="artifact.path"
rel="nofollow"
download
>
Download {{ artifact.name }} artifacts
- </a>
+ </gl-link>
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue
index cb14d4400d1..3339b5c13ed 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue
@@ -88,25 +88,25 @@ export default {
class="table-section section-10 js-pipeline-status pipeline-status"
role="rowheader"
>
- Status
+ {{ s__('Pipeline|Status') }}
</div>
<div
class="table-section section-15 js-pipeline-info pipeline-info"
role="rowheader"
>
- Pipeline
+ {{ s__('Pipeline|Pipeline') }}
</div>
<div
class="table-section section-20 js-pipeline-commit pipeline-commit"
role="rowheader"
>
- Commit
+ {{ s__('Pipeline|Commit') }}
</div>
<div
class="table-section section-20 js-pipeline-stages pipeline-stages"
role="rowheader"
>
- Stages
+ {{ s__('Pipeline|Stages') }}
</div>
</div>
<pipelines-table-row-component
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
index 4f89ee66023..fd674a8d447 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
@@ -261,7 +261,7 @@ export default {
class="table-mobile-header"
role="rowheader"
>
- Status
+ {{ s__('Pipeline|Status') }}
</div>
<div class="table-mobile-content">
<ci-badge
@@ -279,8 +279,9 @@ export default {
<div class="table-section section-20">
<div
class="table-mobile-header"
- role="rowheader">
- Commit
+ role="rowheader"
+ >
+ {{ s__('Pipeline|Commit') }}
</div>
<div class="table-mobile-content">
<commit-component
@@ -298,15 +299,17 @@ export default {
<div class="table-section section-wrap section-20 stage-cell">
<div
class="table-mobile-header"
- role="rowheader">
- Stages
+ role="rowheader"
+ >
+ {{ s__('Pipeline|Stages') }}
</div>
<div class="table-mobile-content">
<template v-if="pipeline.details.stages.length > 0">
<div
v-for="(stage, index) in pipeline.details.stages"
:key="index"
- class="stage-container dropdown js-mini-pipeline-graph">
+ class="stage-container dropdown js-mini-pipeline-graph"
+ >
<pipeline-stage
:type="$options.pipelinesTable"
:stage="stage"
diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue
index 7ec55792850..587c4ffa45c 100644
--- a/app/assets/javascripts/pipelines/components/stage.vue
+++ b/app/assets/javascripts/pipelines/components/stage.vue
@@ -13,23 +13,24 @@
*/
import $ from 'jquery';
+import { GlLoadingIcon, GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import { __ } from '../../locale';
import Flash from '../../flash';
import axios from '../../lib/utils/axios_utils';
import eventHub from '../event_hub';
import Icon from '../../vue_shared/components/icon.vue';
import JobItem from './graph/job_item.vue';
-import tooltip from '../../vue_shared/directives/tooltip';
import { PIPELINES_TABLE } from '../constants';
export default {
components: {
Icon,
JobItem,
+ GlLoadingIcon,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
@@ -157,11 +158,10 @@ export default {
<button
id="stageDropdown"
ref="dropdown"
- v-tooltip
+ v-gl-tooltip.hover
:class="triggerButtonClass"
:title="stage.title"
class="mini-pipeline-graph-dropdown-toggle js-builds-dropdown-button"
- data-placement="top"
data-toggle="dropdown"
data-display="static"
type="button"
diff --git a/app/assets/javascripts/pipelines/components/time_ago.vue b/app/assets/javascripts/pipelines/components/time_ago.vue
index cd43d78de40..bed690200b8 100644
--- a/app/assets/javascripts/pipelines/components/time_ago.vue
+++ b/app/assets/javascripts/pipelines/components/time_ago.vue
@@ -60,7 +60,7 @@ export default {
class="table-mobile-header"
role="rowheader"
>
- Duration
+ {{ s__('Pipeline|Duration') }}
</div>
<div class="table-mobile-content">
<p
@@ -87,7 +87,8 @@ export default {
v-tooltip
:title="tooltipTitle(finishedTime)"
data-placement="top"
- data-container="body">
+ data-container="body"
+ >
{{ timeFormated(finishedTime) }}
</time>
</p>
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js
index 8929b397f6c..41bc5dcce5c 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines.js
@@ -1,4 +1,5 @@
import Visibility from 'visibilityjs';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import { __ } from '../../locale';
import Flash from '../../flash';
import Poll from '../../lib/utils/poll';
@@ -13,6 +14,7 @@ export default {
PipelinesTableComponent,
SvgBlankState,
EmptyState,
+ GlLoadingIcon,
},
data() {
return {
@@ -23,6 +25,15 @@ export default {
hasMadeRequest: false,
};
},
+ computed: {
+ shouldRenderPagination() {
+ return (
+ !this.isLoading &&
+ this.state.pipelines.length &&
+ this.state.pageInfo.total > this.state.pageInfo.perPage
+ );
+ },
+ },
beforeMount() {
this.poll = new Poll({
resource: this.service,
@@ -65,6 +76,35 @@ export default {
this.poll.stop();
},
methods: {
+ /**
+ * Handles URL and query parameter changes.
+ * When the user uses the pagination or the tabs,
+ * - update URL
+ * - Make API request to the server with new parameters
+ * - Update the polling function
+ * - Update the internal state
+ */
+ updateContent(parameters) {
+ this.updateInternalState(parameters);
+
+ // fetch new data
+ return this.service
+ .getPipelines(this.requestData)
+ .then(response => {
+ this.isLoading = false;
+ this.successCallback(response);
+
+ // restart polling
+ this.poll.restart({ data: this.requestData });
+ })
+ .catch(() => {
+ this.isLoading = false;
+ this.errorCallback();
+
+ // restart polling
+ this.poll.restart({ data: this.requestData });
+ });
+ },
updateTable() {
// Cancel ongoing request
if (this.isMakingRequest) {
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js
index d5266544307..f5dae5ad808 100644
--- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js
@@ -2,6 +2,7 @@ import _ from 'underscore';
import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import store from '../store';
@@ -11,6 +12,7 @@ export default {
DropdownButton,
DropdownSearchInput,
DropdownHiddenInput,
+ GlLoadingIcon,
},
props: {
fieldId: {
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index ebe18b47e4e..998554d1be5 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -4,8 +4,10 @@ import { slugifyWithHyphens } from '../lib/utils/text_utility';
let hasUserDefinedProjectPath = false;
-const deriveProjectPathFromUrl = ($projectImportUrl) => {
- const $currentProjectPath = $projectImportUrl.parents('.toggle-import-form').find('#project_path');
+const deriveProjectPathFromUrl = $projectImportUrl => {
+ const $currentProjectPath = $projectImportUrl
+ .parents('.toggle-import-form')
+ .find('#project_path');
if (hasUserDefinedProjectPath) {
return;
}
@@ -52,9 +54,11 @@ const bindEvents = () => {
return;
}
- $('.how_to_import_link').on('click', (e) => {
+ $('.how_to_import_link').on('click', e => {
e.preventDefault();
- $(e.currentTarget).next('.modal').show();
+ $(e.currentTarget)
+ .next('.modal')
+ .show();
});
$('.modal-header .close').on('click', () => {
@@ -63,15 +67,21 @@ const bindEvents = () => {
$('.btn_import_gitlab_project').on('click', () => {
const importHref = $('a.btn_import_gitlab_project').attr('href');
- $('.btn_import_gitlab_project')
- .attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&name=${$projectName.val()}&path=${$projectPath.val()}`);
+ $('.btn_import_gitlab_project').attr(
+ 'href',
+ `${importHref}?namespace_id=${$(
+ '#project_namespace_id',
+ ).val()}&name=${$projectName.val()}&path=${$projectPath.val()}`,
+ );
});
if ($pushNewProjectTipTrigger) {
$pushNewProjectTipTrigger
.removeAttr('rel')
.removeAttr('target')
- .on('click', (e) => { e.preventDefault(); })
+ .on('click', e => {
+ e.preventDefault();
+ })
.popover({
title: $pushNewProjectTipTrigger.data('title'),
placement: 'bottom',
@@ -79,13 +89,15 @@ const bindEvents = () => {
content: $('.push-new-project-tip-template').html(),
})
.on('shown.bs.popover', () => {
- $(document).on('click.popover touchstart.popover', (event) => {
+ $(document).on('click.popover touchstart.popover', event => {
if ($(event.target).closest('.popover').length === 0) {
$pushNewProjectTipTrigger.trigger('click');
}
});
- const target = $(`#${$pushNewProjectTipTrigger.attr('aria-describedby')}`).find('.js-select-on-focus');
+ const target = $(`#${$pushNewProjectTipTrigger.attr('aria-describedby')}`).find(
+ '.js-select-on-focus',
+ );
addSelectOnFocusBehaviour(target);
target.focus();
@@ -117,16 +129,18 @@ const bindEvents = () => {
const selectedTemplate = templates[value];
$selectedTemplateText.text(selectedTemplate.text);
- $(selectedTemplate.icon).clone().addClass('d-block').appendTo($selectedIcon);
+ $(selectedTemplate.icon)
+ .clone()
+ .addClass('d-block')
+ .appendTo($selectedIcon);
const $activeTabProjectName = $('.tab-pane.active #project_name');
const $activeTabProjectPath = $('.tab-pane.active #project_path');
$activeTabProjectName.focus();
- $activeTabProjectName
- .keyup(() => {
- onProjectNameChange($activeTabProjectName, $activeTabProjectPath);
- hasUserDefinedProjectPath = $activeTabProjectPath.val().trim().length > 0;
- });
+ $activeTabProjectName.keyup(() => {
+ onProjectNameChange($activeTabProjectName, $activeTabProjectPath);
+ hasUserDefinedProjectPath = $activeTabProjectPath.val().trim().length > 0;
+ });
}
$useTemplateBtn.on('change', chooseTemplate);
diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
index 120b4fc2f2b..9a729ca9b91 100644
--- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
+++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
@@ -5,6 +5,7 @@ import Poll from '~/lib/utils/poll';
import Flash from '~/flash';
import { s__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import CommitPipelineService from '../services/commit_pipeline_service';
export default {
@@ -13,6 +14,7 @@ export default {
},
components: {
ciIcon,
+ GlLoadingIcon,
},
props: {
endpoint: {
diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue
index 9dd1c87a87d..0a906f40f5a 100644
--- a/app/assets/javascripts/registry/components/app.vue
+++ b/app/assets/javascripts/registry/components/app.vue
@@ -1,5 +1,6 @@
<script>
import { mapGetters, mapActions } from 'vuex';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import Flash from '../../flash';
import store from '../stores';
import collapsibleContainer from './collapsible_container.vue';
@@ -9,6 +10,7 @@ export default {
name: 'RegistryListApp',
components: {
collapsibleContainer,
+ GlLoadingIcon,
},
props: {
endpoint: {
diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue
index 501b2625ae5..be9816a55c4 100644
--- a/app/assets/javascripts/registry/components/collapsible_container.vue
+++ b/app/assets/javascripts/registry/components/collapsible_container.vue
@@ -1,5 +1,6 @@
<script>
import { mapActions } from 'vuex';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import Flash from '../../flash';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import tooltip from '../../vue_shared/directives/tooltip';
@@ -12,6 +13,7 @@ export default {
components: {
clipboardButton,
tableRegistry,
+ GlLoadingIcon,
},
directives: {
tooltip,
diff --git a/app/assets/javascripts/reports/components/issues_list.vue b/app/assets/javascripts/reports/components/issues_list.vue
index 3b425ee2fed..f4243522ef8 100644
--- a/app/assets/javascripts/reports/components/issues_list.vue
+++ b/app/assets/javascripts/reports/components/issues_list.vue
@@ -1,18 +1,31 @@
<script>
-import IssuesBlock from '~/reports/components/report_issues.vue';
-import { STATUS_SUCCESS, STATUS_FAILED, STATUS_NEUTRAL } from '~/reports/constants';
+import ReportItem from '~/reports/components/report_item.vue';
+import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants';
+import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
+
+const wrapIssueWithState = (status, isNew = false) => issue => ({
+ status: issue.status || status,
+ isNew,
+ issue,
+});
/**
* Renders block of issues
*/
-
export default {
components: {
- IssuesBlock,
+ SmartVirtualList,
+ ReportItem,
},
- success: STATUS_SUCCESS,
- failed: STATUS_FAILED,
- neutral: STATUS_NEUTRAL,
+ // Typical height of a report item in px
+ typicalReportItemHeight: 32,
+ /*
+ The maximum amount of shown issues. This is calculated by
+ ( max-height of report-block-list / typicalReportItemHeight ) + some safety margin
+ We will use VirtualList if we have more items than this number.
+ For entries lower than this number, the virtual scroll list calculates the total height of the element wrongly.
+ */
+ maxShownReportItems: 20,
props: {
newIssues: {
type: Array,
@@ -40,42 +53,34 @@ export default {
default: '',
},
},
+ computed: {
+ issuesWithState() {
+ return [
+ ...this.newIssues.map(wrapIssueWithState(STATUS_FAILED, true)),
+ ...this.unresolvedIssues.map(wrapIssueWithState(STATUS_FAILED)),
+ ...this.neutralIssues.map(wrapIssueWithState(STATUS_NEUTRAL)),
+ ...this.resolvedIssues.map(wrapIssueWithState(STATUS_SUCCESS)),
+ ];
+ },
+ },
};
</script>
<template>
- <div class="report-block-container">
-
- <issues-block
- v-if="newIssues.length"
- :component="component"
- :issues="newIssues"
- class="js-mr-code-new-issues"
- status="failed"
- is-new
- />
-
- <issues-block
- v-if="unresolvedIssues.length"
- :component="component"
- :issues="unresolvedIssues"
- :status="$options.failed"
- class="js-mr-code-new-issues"
- />
-
- <issues-block
- v-if="neutralIssues.length"
- :component="component"
- :issues="neutralIssues"
- :status="$options.neutral"
- class="js-mr-code-non-issues"
- />
-
- <issues-block
- v-if="resolvedIssues.length"
+ <smart-virtual-list
+ :length="issuesWithState.length"
+ :remain="$options.maxShownReportItems"
+ :size="$options.typicalReportItemHeight"
+ class="report-block-container"
+ wtag="ul"
+ wclass="report-block-list"
+ >
+ <report-item
+ v-for="(wrapped, index) in issuesWithState"
+ :key="index"
+ :issue="wrapped.issue"
+ :status="wrapped.status"
:component="component"
- :issues="resolvedIssues"
- :status="$options.success"
- class="js-mr-code-resolved-issues"
+ :is-new="wrapped.isNew"
/>
- </div>
+ </smart-virtual-list>
</template>
diff --git a/app/assets/javascripts/reports/components/report_issues.vue b/app/assets/javascripts/reports/components/report_item.vue
index a2a03945ae3..01e6d357a21 100644
--- a/app/assets/javascripts/reports/components/report_issues.vue
+++ b/app/assets/javascripts/reports/components/report_item.vue
@@ -3,14 +3,14 @@ import IssueStatusIcon from '~/reports/components/issue_status_icon.vue';
import { components, componentNames } from '~/reports/components/issue_body';
export default {
- name: 'ReportIssues',
+ name: 'ReportItem',
components: {
IssueStatusIcon,
...components,
},
props: {
- issues: {
- type: Array,
+ issue: {
+ type: Object,
required: true,
},
component: {
@@ -33,27 +33,21 @@ export default {
};
</script>
<template>
- <div>
- <ul class="report-block-list">
- <li
- v-for="(issue, index) in issues"
- :key="index"
- :class="{ 'is-dismissed': issue.isDismissed }"
- class="report-block-list-issue"
- >
- <issue-status-icon
- :status="issue.status || status"
- class="append-right-5"
- />
+ <li
+ :class="{ 'is-dismissed': issue.isDismissed }"
+ class="report-block-list-issue"
+ >
+ <issue-status-icon
+ :status="status"
+ class="append-right-5"
+ />
- <component
- :is="component"
- v-if="component"
- :issue="issue"
- :status="issue.status || status"
- :is-new="isNew"
- />
- </li>
- </ul>
- </div>
+ <component
+ :is="component"
+ v-if="component"
+ :issue="issue"
+ :status="status"
+ :is-new="isNew"
+ />
+ </li>
</template>
diff --git a/app/assets/javascripts/reports/components/summary_row.vue b/app/assets/javascripts/reports/components/summary_row.vue
index 51188981bed..a44ba833b63 100644
--- a/app/assets/javascripts/reports/components/summary_row.vue
+++ b/app/assets/javascripts/reports/components/summary_row.vue
@@ -1,6 +1,7 @@
<script>
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Popover from '~/vue_shared/components/help_popover.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
/**
* Renders the summary row for each report
@@ -15,6 +16,7 @@ export default {
components: {
CiIcon,
Popover,
+ GlLoadingIcon,
},
props: {
summary: {
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 6b3753f7966..225e21ad322 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -21,7 +21,7 @@ Sidebar.initialize = function(currentUser) {
}
};
-Sidebar.prototype.removeListeners = function () {
+Sidebar.prototype.removeListeners = function() {
this.sidebar.off('click', '.sidebar-collapsed-icon');
this.sidebar.off('hidden.gl.dropdown');
$('.dropdown').off('loading.gl.dropdown');
@@ -38,10 +38,12 @@ Sidebar.prototype.addEventListeners = function() {
$('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
$document.on('click', '.js-sidebar-toggle', this.sidebarToggleClicked);
- return $(document).off('click', '.js-issuable-todo').on('click', '.js-issuable-todo', this.toggleTodo);
+ return $(document)
+ .off('click', '.js-issuable-todo')
+ .on('click', '.js-issuable-todo', this.toggleTodo);
};
-Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
+Sidebar.prototype.sidebarToggleClicked = function(e, triggered) {
var $allGutterToggleIcons, $this, isExpanded, tooltipLabel;
e.preventDefault();
$this = $(this);
@@ -51,18 +53,26 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
if (isExpanded) {
$allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left');
- $('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
- $('.layout-page').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
+ $('aside.right-sidebar')
+ .removeClass('right-sidebar-expanded')
+ .addClass('right-sidebar-collapsed');
+ $('.layout-page')
+ .removeClass('right-sidebar-expanded')
+ .addClass('right-sidebar-collapsed');
} else {
$allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right');
- $('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
- $('.layout-page').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
+ $('aside.right-sidebar')
+ .removeClass('right-sidebar-collapsed')
+ .addClass('right-sidebar-expanded');
+ $('.layout-page')
+ .removeClass('right-sidebar-collapsed')
+ .addClass('right-sidebar-expanded');
}
$this.attr('data-original-title', tooltipLabel);
if (!triggered) {
- Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'));
+ Cookies.set('collapsed_gutter', $('.right-sidebar').hasClass('right-sidebar-collapsed'));
}
};
@@ -71,21 +81,27 @@ Sidebar.prototype.toggleTodo = function(e) {
$this = $(e.currentTarget);
ajaxType = $this.attr('data-delete-path') ? 'delete' : 'post';
if ($this.attr('data-delete-path')) {
- url = "" + ($this.attr('data-delete-path'));
+ url = '' + $this.attr('data-delete-path');
} else {
- url = "" + ($this.data('url'));
+ url = '' + $this.data('url');
}
$this.tooltip('hide');
- $('.js-issuable-todo').disable().addClass('is-loading');
+ $('.js-issuable-todo')
+ .disable()
+ .addClass('is-loading');
axios[ajaxType](url, {
issuable_id: $this.data('issuableId'),
issuable_type: $this.data('issuableType'),
- }).then(({ data }) => {
- this.todoUpdateDone(data);
- }).catch(() => flash(`There was an error ${ajaxType === 'post' ? 'adding a' : 'deleting the'} todo.`));
+ })
+ .then(({ data }) => {
+ this.todoUpdateDone(data);
+ })
+ .catch(() =>
+ flash(`There was an error ${ajaxType === 'post' ? 'adding a' : 'deleting the'} todo.`),
+ );
};
Sidebar.prototype.todoUpdateDone = function(data) {
@@ -99,7 +115,8 @@ Sidebar.prototype.todoUpdateDone = function(data) {
const $el = $(el);
const $elText = $el.find('.js-issuable-todo-inner');
- $el.removeClass('is-loading')
+ $el
+ .removeClass('is-loading')
.enable()
.attr('aria-label', $el.data(`${attrPrefix}Text`))
.attr('data-delete-path', deletePath)
@@ -119,7 +136,9 @@ Sidebar.prototype.todoUpdateDone = function(data) {
Sidebar.prototype.sidebarDropdownLoading = function(e) {
var $loading, $sidebarCollapsedIcon, i, img;
- $sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon');
+ $sidebarCollapsedIcon = $(this)
+ .closest('.block')
+ .find('.sidebar-collapsed-icon');
img = $sidebarCollapsedIcon.find('img');
i = $sidebarCollapsedIcon.find('i');
$loading = $('<i class="fa fa-spinner fa-spin"></i>');
@@ -134,7 +153,9 @@ Sidebar.prototype.sidebarDropdownLoading = function(e) {
Sidebar.prototype.sidebarDropdownLoaded = function(e) {
var $sidebarCollapsedIcon, i, img;
- $sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon');
+ $sidebarCollapsedIcon = $(this)
+ .closest('.block')
+ .find('.sidebar-collapsed-icon');
img = $sidebarCollapsedIcon.find('img');
$sidebarCollapsedIcon.find('i.fa-spin').remove();
i = $sidebarCollapsedIcon.find('i');
@@ -220,7 +241,7 @@ Sidebar.prototype.isOpen = function() {
};
Sidebar.prototype.getBlock = function(name) {
- return this.sidebar.find(".block." + name);
+ return this.sidebar.find('.block.' + name);
};
export default Sidebar;
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index 7bde4860973..17def77b2d7 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -226,7 +226,7 @@ export class SearchAutocomplete {
icon,
text: term,
template: s__('SearchAutocomplete|in all GitLab'),
- url: `/search?search=${term}`,
+ url: `${gon.relative_url_root}/search?search=${term}`,
});
if (template) {
@@ -234,7 +234,9 @@ export class SearchAutocomplete {
icon,
text: term,
template,
- url: `/search?search=${term}&project_id=${this.projectInputEl.val()}&group_id=${this.groupInputEl.val()}`,
+ url: `${
+ gon.relative_url_root
+ }/search?search=${term}&project_id=${this.projectInputEl.val()}&group_id=${this.groupInputEl.val()}`,
});
}
}
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 43f0b6651b9..4d461baf74d 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
@@ -5,6 +5,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import GfmAutoComplete from '~/gfm_auto_complete';
import { __, s__ } from '~/locale';
import Api from '~/api';
+import { GlModal, GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import eventHub from './event_hub';
import EmojiMenuInModal from './emoji_menu_in_modal';
@@ -13,6 +14,10 @@ const emojiMenuClass = 'js-modal-status-emoji-menu';
export default {
components: {
Icon,
+ GlModal,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
},
props: {
currentEmoji: {
@@ -152,7 +157,7 @@ export default {
</script>
<template>
- <gl-ui-modal
+ <gl-modal
:title="s__('SetStatusModal|Set a status')"
:modal-id="modalId"
:ok-title="s__('SetStatusModal|Set status')"
@@ -237,5 +242,5 @@ export default {
</div>
</div>
</div>
- </gl-ui-modal>
+ </gl-modal>
</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
index dd155c133ce..f1ea6aacdb2 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
@@ -74,8 +74,8 @@ export default {
}
if (!this.users.length) {
- const emptyTooltipLabel = this.issuableType === 'issue' ?
- __('Assignee(s)') : __('Assignee');
+ const emptyTooltipLabel =
+ this.issuableType === 'issue' ? __('Assignee(s)') : __('Assignee');
names.push(emptyTooltipLabel);
}
@@ -248,4 +248,3 @@ export default {
</div>
</div>
</template>
-
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
index 11b5dbe5f8e..fe73f6a0cef 100644
--- a/app/assets/javascripts/sidebar/components/participants/participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -2,6 +2,7 @@
import { __, n__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
directives: {
@@ -9,6 +10,7 @@ export default {
},
components: {
userAvatarImage,
+ GlLoadingIcon,
},
props: {
loading: {
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
index 448c8fc3602..b6151aa6c64 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
@@ -1,74 +1,74 @@
<script>
- import { __ } from '~/locale';
- import icon from '~/vue_shared/components/icon.vue';
- import toggleButton from '~/vue_shared/components/toggle_button.vue';
- import tooltip from '~/vue_shared/directives/tooltip';
- import eventHub from '../../event_hub';
+import { __ } from '~/locale';
+import icon from '~/vue_shared/components/icon.vue';
+import toggleButton from '~/vue_shared/components/toggle_button.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import eventHub from '../../event_hub';
- const ICON_ON = 'notifications';
- const ICON_OFF = 'notifications-off';
- const LABEL_ON = __('Notifications on');
- const LABEL_OFF = __('Notifications off');
+const ICON_ON = 'notifications';
+const ICON_OFF = 'notifications-off';
+const LABEL_ON = __('Notifications on');
+const LABEL_OFF = __('Notifications off');
- export default {
- directives: {
- tooltip,
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ icon,
+ toggleButton,
+ },
+ props: {
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- components: {
- icon,
- toggleButton,
+ subscribed: {
+ type: Boolean,
+ required: false,
+ default: null,
},
- props: {
- loading: {
- type: Boolean,
- required: false,
- default: false,
- },
- subscribed: {
- type: Boolean,
- required: false,
- default: null,
- },
- id: {
- type: Number,
- required: false,
- default: null,
- },
+ id: {
+ type: Number,
+ required: false,
+ default: null,
},
- computed: {
- showLoadingState() {
- return this.subscribed === null;
- },
- notificationIcon() {
- return this.subscribed ? ICON_ON : ICON_OFF;
- },
- notificationTooltip() {
- return this.subscribed ? LABEL_ON : LABEL_OFF;
- },
+ },
+ computed: {
+ showLoadingState() {
+ return this.subscribed === null;
},
- methods: {
- /**
- * We need to emit this event on both component & eventHub
- * for 2 dependencies;
- *
- * 1. eventHub: This component is used in Issue Boards sidebar
- * where component template is part of HAML
- * and event listeners are tied to app's eventHub.
- * 2. Component: This compone is also used in Epics in EE
- * where listeners are tied to component event.
- */
- toggleSubscription() {
- // App's eventHub event emission.
- eventHub.$emit('toggleSubscription', this.id);
+ notificationIcon() {
+ return this.subscribed ? ICON_ON : ICON_OFF;
+ },
+ notificationTooltip() {
+ return this.subscribed ? LABEL_ON : LABEL_OFF;
+ },
+ },
+ methods: {
+ /**
+ * We need to emit this event on both component & eventHub
+ * for 2 dependencies;
+ *
+ * 1. eventHub: This component is used in Issue Boards sidebar
+ * where component template is part of HAML
+ * and event listeners are tied to app's eventHub.
+ * 2. Component: This compone is also used in Epics in EE
+ * where listeners are tied to component event.
+ */
+ toggleSubscription() {
+ // App's eventHub event emission.
+ eventHub.$emit('toggleSubscription', this.id);
- // Component event emission.
- this.$emit('toggleSubscription', this.id);
- },
- onClickCollapsedIcon() {
- this.$emit('toggleSidebar');
- },
+ // Component event emission.
+ this.$emit('toggleSubscription', this.id);
+ },
+ onClickCollapsedIcon() {
+ this.$emit('toggleSidebar');
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
index e74912d628f..b145e5dc5e2 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
@@ -1,9 +1,13 @@
<script>
import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
import tooltip from '../../../vue_shared/directives/tooltip';
+import { GlProgressBar } from '@gitlab-org/gitlab-ui';
export default {
name: 'TimeTrackingComparisonPane',
+ components: {
+ GlProgressBar,
+ },
directives: {
tooltip,
},
diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
index bc59774f0a8..913a616d9f1 100644
--- a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
+++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
@@ -1,6 +1,7 @@
<script>
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
@@ -13,6 +14,7 @@ export default {
},
components: {
Icon,
+ GlLoadingIcon,
},
props: {
issuableId: {
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index d9ca5e46770..3e040ec8428 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -39,9 +39,10 @@ export default class SidebarMediator {
}
fetch() {
- return this.service.get()
+ return this.service
+ .get()
.then(response => response.json())
- .then((data) => {
+ .then(data => {
this.processFetchedData(data);
})
.catch(() => new Flash('Error occurred when fetching sidebar data'));
@@ -56,30 +57,33 @@ export default class SidebarMediator {
toggleSubscription() {
this.store.setFetchingState('subscriptions', true);
- return this.service.toggleSubscription()
+ return this.service
+ .toggleSubscription()
.then(() => {
this.store.setSubscribedState(!this.store.subscribed);
this.store.setFetchingState('subscriptions', false);
})
- .catch((err) => {
+ .catch(err => {
this.store.setFetchingState('subscriptions', false);
throw err;
});
}
fetchAutocompleteProjects(searchTerm) {
- return this.service.getProjectsAutocomplete(searchTerm)
+ return this.service
+ .getProjectsAutocomplete(searchTerm)
.then(response => response.json())
- .then((data) => {
+ .then(data => {
this.store.setAutocompleteProjects(data);
return this.store.autocompleteProjects;
});
}
moveIssue() {
- return this.service.moveIssue(this.store.moveToProjectId)
+ return this.service
+ .moveIssue(this.store.moveToProjectId)
.then(response => response.json())
- .then((data) => {
+ .then(data => {
if (window.location.pathname !== data.web_url) {
visitUrl(data.web_url);
}
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 4b090212d83..ce051582299 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, one-var, no-var, prefer-rest-params, vars-on-top, prefer-arrow-callback, consistent-return, object-shorthand, no-shadow, no-unused-vars, no-else-return, no-self-compare, prefer-template, no-unused-expressions, yoda, prefer-spread, no-void, camelcase, no-param-reassign */
+/* eslint-disable func-names, one-var, no-var, prefer-rest-params, vars-on-top, prefer-arrow-callback, consistent-return, object-shorthand, no-shadow, no-unused-vars, no-else-return, no-self-compare, prefer-template, no-unused-expressions, yoda, prefer-spread, camelcase, no-param-reassign */
/* global Issuable */
/* global emitSidebarEvent */
@@ -696,17 +696,21 @@ UsersSelect.prototype.formatResult = function(user) {
} else {
avatar = gon.default_avatar_url;
}
- return (
- "<div class='user-result " +
- (!user.username ? 'no-username' : void 0) +
- "'> <div class='user-image'><img class='avatar avatar-inline s32' src='" +
- avatar +
- "'></div> <div class='user-name dropdown-menu-user-full-name'>" +
- _.escape(user.name) +
- "</div> <div class='user-username dropdown-menu-user-username'>" +
- (!user.invite ? '@' + _.escape(user.username) : '') +
- '</div> </div>'
- );
+ return `
+ <div class='user-result'>
+ <div class='user-image'>
+ <img class='avatar avatar-inline s32' src='${avatar}'>
+ </div>
+ <div class='user-info'>
+ <div class='user-name dropdown-menu-user-full-name'>
+ ${_.escape(user.name)}
+ </div>
+ <div class='user-username dropdown-menu-user-username text-secondary'>
+ ${!user.invite ? '@' + _.escape(user.username) : ''}
+ </div>
+ </div>
+ </div>
+ `;
};
UsersSelect.prototype.formatSelection = function(user) {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
index 5e7a35e9396..4ec925aa8a6 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
@@ -10,6 +10,7 @@ import { visitUrl } from '../../lib/utils/url_utility';
import createFlash from '../../flash';
import MemoryUsage from './memory_usage.vue';
import StatusIcon from './mr_widget_status_icon.vue';
+import ReviewAppLink from './review_app_link.vue';
import MRWidgetService from '../services/mr_widget_service';
export default {
@@ -21,6 +22,7 @@ export default {
Icon,
TooltipOnTruncate,
FilteredSearchDropdown,
+ ReviewAppLink,
},
directives: {
tooltip,
@@ -38,10 +40,8 @@ export default {
failed: __('Failed to deploy to'),
},
data() {
- const features = window.gon.features || {};
return {
isStopping: false,
- enableCiEnvironmentsStatusChanges: features.ciEnvironmentsStatusChanges,
};
},
computed: {
@@ -63,6 +63,17 @@ export default {
deployedText() {
return this.$options.deployedTextMap[this.deployment.status];
},
+ isDeployInProgress() {
+ return this.deployment.status === 'running';
+ },
+ deployInProgressTooltip() {
+ return this.isDeployInProgress
+ ? __('Stopping this environment is currently not possible as a deployment is in progress')
+ : '';
+ },
+ shouldRenderDropdown() {
+ return this.deployment.changes && this.deployment.changes.length > 0;
+ },
},
methods: {
stopEnvironment() {
@@ -133,7 +144,7 @@ export default {
<div>
<template v-if="hasExternalUrls">
<filtered-search-dropdown
- v-if="enableCiEnvironmentsStatusChanges"
+ v-if="shouldRenderDropdown"
class="js-mr-wigdet-deployment-dropdown inline"
:items="deployment.changes"
:main-action-link="deployment.external_url"
@@ -143,18 +154,10 @@ export default {
slot="mainAction"
slot-scope="slotProps"
>
- <a
- :href="deployment.external_url"
- target="_blank"
- rel="noopener noreferrer nofollow"
- class="deploy-link js-deploy-url inline"
- :class="slotProps.className"
- >
- <span>
- {{ __('View app') }}
- <icon name="external-link" />
- </span>
- </a>
+ <review-app-link
+ :link="deployment.external_url"
+ :css-class="`deploy-link js-deploy-url inline ${slotProps.className}`"
+ />
</template>
<template
@@ -177,28 +180,29 @@ export default {
</a>
</template>
</filtered-search-dropdown>
- <a
+ <review-app-link
v-else
- :href="deployment.external_url"
- target="_blank"
- rel="noopener noreferrer nofollow"
- class="js-deploy-url js-deploy-url-feature-flag deploy-link btn btn-default btn-sm inline"
- >
- <span>
- {{ __('View app') }}
- <icon name="external-link" />
- </span>
- </a>
+ :link="deployment.external_url"
+ css-class="js-deploy-url js-deploy-url-feature-flag deploy-link btn btn-default btn-sm inlin"
+ />
</template>
- <loading-button
+ <span
v-if="deployment.stop_url"
- :loading="isStopping"
- container-class="btn btn-default btn-sm inline prepend-left-4"
- title="Stop environment"
- @click="stopEnvironment"
+ v-tooltip
+ :title="deployInProgressTooltip"
+ class="d-inline-block"
+ tabindex="0"
>
- <icon name="stop" />
- </loading-button>
+ <loading-button
+ :loading="isStopping"
+ :disabled="isDeployInProgress"
+ :title="__('Stop environment')"
+ container-class="js-stop-env btn btn-default btn-sm inline prepend-left-4"
+ @click="stopEnvironment"
+ >
+ <icon name="stop" />
+ </loading-button>
+ </span>
</div>
</div>
</div>
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 fee41b239e8..53608838f2f 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
@@ -1,5 +1,6 @@
<script>
/* eslint-disable vue/require-default-prop */
+import { sprintf, __ } from '~/locale';
import PipelineStage from '~/pipelines/components/stage.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
@@ -36,6 +37,10 @@ export default {
type: String,
required: false,
},
+ troubleshootingDocsPath: {
+ type: String,
+ required: true,
+ },
},
computed: {
hasPipeline() {
@@ -57,6 +62,18 @@ export default {
hasCommitInfo() {
return this.pipeline.commit && Object.keys(this.pipeline.commit).length > 0;
},
+ errorText() {
+ return sprintf(
+ __(
+ 'Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation.%{linkEnd}',
+ ),
+ {
+ linkStart: `<a href="${this.troubleshootingDocsPath}">`,
+ linkEnd: '</a>',
+ },
+ false,
+ );
+ },
},
};
</script>
@@ -77,8 +94,10 @@ export default {
name="status_failed_borderless"
/>
</div>
- <div class="media-body">
- Could not connect to the CI server. Please check your settings and try again
+ <div
+ class="media-body"
+ v-html="errorText"
+ >
</div>
</template>
<template v-else-if="hasPipeline">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
index ba6a1687e51..b3340290ed3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
@@ -1,9 +1,11 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import ciIcon from '../../vue_shared/components/ci_icon.vue';
export default {
components: {
ciIcon,
+ GlLoadingIcon,
},
props: {
status: {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue b/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue
new file mode 100644
index 00000000000..b007d4f4dcb
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue
@@ -0,0 +1,30 @@
+<script>
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ components: {
+ Icon,
+ },
+ props: {
+ link: {
+ type: String,
+ required: true,
+ },
+ cssClass: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <a
+ :href="link"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ :class="cssClass"
+ >
+ {{ __('View app') }}
+ <icon name="external-link" />
+ </a>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
index 4f8b07484c0..4bfbdcf1404 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
@@ -1,4 +1,5 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import eventHub from '../../event_hub';
import statusIcon from '../mr_widget_status_icon.vue';
@@ -6,6 +7,7 @@ export default {
name: 'MRWidgetAutoMergeFailed',
components: {
statusIcon,
+ GlLoadingIcon,
},
props: {
mr: {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
index 656c3b5c47e..7e33021e4b4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
@@ -6,6 +6,7 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import MrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
name: 'MRWidgetMerged',
@@ -16,6 +17,7 @@ export default {
MrWidgetAuthorTime,
statusIcon,
ClipboardButton,
+ GlLoadingIcon,
},
props: {
mr: {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
index 041fa13a8f5..0e714cc2aa1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
@@ -1,4 +1,5 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import simplePoll from '../../../lib/utils/simple_poll';
import eventHub from '../../event_hub';
import statusIcon from '../mr_widget_status_icon.vue';
@@ -8,6 +9,7 @@ export default {
name: 'MRWidgetRebase',
components: {
statusIcon,
+ GlLoadingIcon,
},
props: {
mr: {
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 c8ad2aa30a6..e7baecbcde4 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
@@ -71,7 +71,12 @@ export default {
return defaultClass;
},
iconClass() {
- if (this.status === 'failed' || !this.commitMessage.length || !this.mr.isMergeAllowed || this.mr.preventMerge) {
+ if (
+ this.status === 'failed' ||
+ !this.commitMessage.length ||
+ !this.mr.isMergeAllowed ||
+ this.mr.preventMerge
+ ) {
return 'warning';
}
return 'success';
@@ -90,10 +95,12 @@ export default {
},
isMergeButtonDisabled() {
const { commitMessage } = this;
- return Boolean(!commitMessage.length
- || !this.shouldShowMergeControls()
- || this.isMakingRequest
- || this.mr.preventMerge);
+ return Boolean(
+ !commitMessage.length ||
+ !this.shouldShowMergeControls() ||
+ this.isMakingRequest ||
+ this.mr.preventMerge,
+ );
},
isRemoveSourceBranchButtonDisabled() {
return this.isMergeButtonDisabled;
@@ -140,9 +147,10 @@ export default {
};
this.isMakingRequest = true;
- this.service.merge(options)
+ this.service
+ .merge(options)
.then(res => res.data)
- .then((data) => {
+ .then(data => {
const hasError = data.status === 'failed' || data.status === 'hook_validation_error';
if (data.status === 'merge_when_pipeline_succeeds') {
@@ -167,9 +175,10 @@ export default {
});
},
handleMergePolling(continuePolling, stopPolling) {
- this.service.poll()
+ this.service
+ .poll()
.then(res => res.data)
- .then((data) => {
+ .then(data => {
if (data.state === 'merged') {
// If state is merged we should update the widget and stop the polling
eventHub.$emit('MRWidgetUpdateRequested');
@@ -205,9 +214,10 @@ export default {
});
},
handleRemoveBranchPolling(continuePolling, stopPolling) {
- this.service.poll()
+ this.service
+ .poll()
.then(res => res.data)
- .then((data) => {
+ .then(data => {
// If source branch exists then we should continue polling
// because removing a source branch is a background task and takes time
if (data.source_branch_exists) {
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 a5c69d2bc7a..063d1e15544 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
@@ -116,7 +116,7 @@ export default {
// init polling
this.initPostMergeDeploymentsPolling();
}
- }
+ },
},
created() {
this.initPolling();
@@ -213,17 +213,21 @@ export default {
})
.catch(() => this.throwDeploymentsError());
},
- fetchPostMergeDeployments(){
+ fetchPostMergeDeployments() {
return this.fetchDeployments('merge_commit')
.then(({ data }) => {
if (data.length) {
this.mr.postMergeDeployments = data;
}
})
- .catch(() => this.throwDeploymentsError());
+ .catch(() => this.throwDeploymentsError());
},
throwDeploymentsError() {
- createFlash(__('Something went wrong while fetching the environments for this merge request. Please try again.'));
+ createFlash(
+ __(
+ 'Something went wrong while fetching the environments for this merge request. Please try again.',
+ ),
+ );
},
fetchActionsContent() {
this.service
@@ -301,6 +305,7 @@ export default {
:has-ci="mr.hasCI"
:source-branch="mr.sourceBranch"
:source-branch-link="mr.sourceBranchLink"
+ :troubleshooting-docs-path="mr.troubleshootingDocsPath"
/>
<deployment
v-for="deployment in mr.deployments"
@@ -355,6 +360,7 @@ export default {
:has-ci="mr.hasCI"
:source-branch="mr.targetBranch"
:source-branch-link="mr.targetBranch"
+ :troubleshooting-docs-path="mr.troubleshootingDocsPath"
/>
<deployment
v-for="postMergeDeployment in mr.postMergeDeployments"
diff --git a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
index bf5b85b2ae6..0bb70bfd658 100644
--- a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
+++ b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
@@ -24,8 +24,8 @@ export default class MRWidgetService {
fetchDeployments(targetParam) {
return axios.get(this.endpoints.ciEnvironmentsStatusPath, {
params: {
- environment_target: targetParam
- }
+ environment_target: targetParam,
+ },
});
}
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 a0c008e7314..5c9a7133a6e 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
@@ -18,6 +18,7 @@ export default class MergeRequestStore {
this.squash = data.squash;
this.squashBeforeMergeHelpPath =
this.squashBeforeMergeHelpPath || data.squash_before_merge_help_path;
+ this.troubleshootingDocsPath = this.troubleshootingDocsPath || data.troubleshooting_docs_path;
this.enableSquashBeforeMerge = this.enableSquashBeforeMerge || true;
this.iid = data.iid;
diff --git a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
index 8684005e0fb..766fc211bf5 100644
--- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
@@ -1,5 +1,5 @@
<script>
-import tooltip from '~/vue_shared/directives/tooltip';
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
import { pluralize } from '~/lib/utils/text_utility';
import { __, sprintf } from '~/locale';
@@ -10,7 +10,7 @@ export default {
Icon,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
file: {
@@ -79,10 +79,8 @@ export default {
<template>
<span
- v-tooltip
+ v-gl-tooltip.right
:title="tooltipTitle"
- data-container="body"
- data-placement="right"
class="file-changed-icon ml-auto"
>
<icon
diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
index c60052fec50..6780254827f 100644
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
@@ -1,6 +1,6 @@
<script>
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import CiIcon from './ci_icon.vue';
-import tooltip from '../directives/tooltip';
/**
* Renders CI Badge link with CI icon and status text based on
* API response shared between all places where it is used.
@@ -27,7 +27,7 @@ export default {
CiIcon,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
status: {
@@ -50,7 +50,7 @@ export default {
</script>
<template>
<a
- v-tooltip
+ v-gl-tooltip
:href="status.details_path"
:class="cssClass"
:title="!showText ? status.text : ''"
diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
index 945a33d9622..6b90a1f540e 100644
--- a/app/assets/javascripts/vue_shared/components/clipboard_button.vue
+++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
@@ -12,20 +12,18 @@
* css-class="btn-transparent"
* />
*/
-import tooltip from '../directives/tooltip';
+import { GlButton, GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import Icon from '../components/icon.vue';
export default {
name: 'ClipboardButton',
-
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
-
components: {
+ GlButton,
Icon,
},
-
props: {
text: {
type: String,
@@ -68,16 +66,12 @@ export default {
</script>
<template>
- <button
- v-tooltip
+ <gl-button
+ v-gl-tooltip="{ placement: tooltipPlacement, container: tooltipContainer }"
:class="cssClass"
:title="title"
:data-clipboard-text="clipboardText"
- :data-container="tooltipContainer"
- :data-placement="tooltipPlacement"
- type="button"
- class="btn"
>
<icon name="duplicate" />
- </button>
+ </gl-button>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue
index 13bca99dcb3..b1139f34e41 100644
--- a/app/assets/javascripts/vue_shared/components/commit.vue
+++ b/app/assets/javascripts/vue_shared/components/commit.vue
@@ -1,11 +1,11 @@
<script>
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import UserAvatarLink from './user_avatar/user_avatar_link.vue';
-import tooltip from '../directives/tooltip';
import Icon from '../../vue_shared/components/icon.vue';
export default {
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
components: {
UserAvatarLink,
@@ -13,7 +13,7 @@ export default {
},
props: {
/**
- * Indicates the existance of a tag.
+ * Indicates the existence of a tag.
* Used to render the correct icon, if true will render `fa-tag` icon,
* if false will render a svg sprite fork icon
*/
@@ -124,11 +124,10 @@ export default {
</div>
<a
- v-tooltip
+ v-gl-tooltip
:href="commitRef.ref_url"
:title="commitRef.name"
class="ref-name"
- data-container="body"
>
{{ commitRef.name }}
</a>
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
index 8163947cd0c..6f2f0f98690 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
@@ -17,19 +17,37 @@ export default {
type: Boolean,
default: true,
},
+ innerCssClasses: {
+ type: [Array, Object, String],
+ required: false,
+ default: '',
+ },
},
data() {
return {
width: 0,
height: 0,
- isZoomable: false,
- isZoomed: false,
+ isLoaded: false,
};
},
computed: {
fileSizeReadable() {
return numberToHumanSize(this.fileSize);
},
+ dimensionStyles() {
+ if (!this.isLoaded) return {};
+
+ return {
+ width: `${this.width}px`,
+ height: `${this.height}px`,
+ };
+ },
+ hasFileSize() {
+ return this.fileSize > 0;
+ },
+ hasDimensions() {
+ return this.width && this.height;
+ },
},
beforeDestroy() {
window.removeEventListener('resize', this.resizeThrottled, false);
@@ -48,51 +66,52 @@ export default {
const { contentImg } = this.$refs;
if (contentImg) {
- this.isZoomable =
- contentImg.naturalWidth > contentImg.width ||
- contentImg.naturalHeight > contentImg.height;
-
this.width = contentImg.naturalWidth;
this.height = contentImg.naturalHeight;
- this.$emit('imgLoaded', {
- width: this.width,
- height: this.height,
- renderedWidth: contentImg.clientWidth,
- renderedHeight: contentImg.clientHeight,
+ this.$nextTick(() => {
+ this.isLoaded = true;
+
+ this.$emit('imgLoaded', {
+ width: this.width,
+ height: this.height,
+ renderedWidth: contentImg.clientWidth,
+ renderedHeight: contentImg.clientHeight,
+ });
});
}
},
- onImgClick() {
- if (this.isZoomable) this.isZoomed = !this.isZoomed;
- },
},
};
</script>
<template>
- <div class="file-container">
- <div class="file-content image_file">
+ <div>
+ <div
+ :class="innerCssClasses"
+ :style="dimensionStyles"
+ class="position-relative"
+ >
<img
ref="contentImg"
- :class="{ 'is-zoomable': isZoomable, 'is-zoomed': isZoomed }"
:src="path"
- :alt="path"
@load="onImgLoad"
- @click="onImgClick"/>
- <p
- v-if="renderInfo"
- class="file-info prepend-top-10">
- <template v-if="fileSize>0">
- {{ fileSizeReadable }}
- </template>
- <template v-if="fileSize>0 && width && height">
- |
- </template>
- <template v-if="width && height">
- W: {{ width }} | H: {{ height }}
- </template>
- </p>
+ />
+ <slot name="image-overlay"></slot>
</div>
+ <p
+ v-if="renderInfo"
+ class="image-info"
+ >
+ <template v-if="hasFileSize">
+ {{ fileSizeReadable }}
+ </template>
+ <template v-if="hasFileSize && hasDimensions">
+ |
+ </template>
+ <template v-if="hasDimensions">
+ <strong>W</strong>: {{ width }} | <strong>H</strong>: {{ height }}
+ </template>
+ </p>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
index cfc5343217c..9c3f3e7f7a9 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
@@ -69,6 +69,13 @@ export default {
:new-path="fullNewPath"
:old-path="fullOldPath"
:project-path="projectPath"
- />
+ >
+ <slot
+ slot="image-overlay"
+ name="image-overlay"
+ >
+ </slot>
+ </component>
+ <slot></slot>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue
index 38e881d17a2..cd0c1e850af 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue
@@ -15,11 +15,6 @@ export default {
type: String,
required: true,
},
- projectPath: {
- type: String,
- required: false,
- default: '',
- },
},
data() {
return {
@@ -120,7 +115,6 @@ export default {
key="onionOldImg"
:render-info="false"
:path="oldPath"
- :project-path="projectPath"
@imgLoaded="onionOldImgLoaded"
/>
</div>
@@ -136,9 +130,14 @@ export default {
key="onionNewImg"
:render-info="false"
:path="newPath"
- :project-path="projectPath"
@imgLoaded="onionNewImgLoaded"
- />
+ >
+ <slot
+ slot="image-overlay"
+ name="image-overlay"
+ >
+ </slot>
+ </image-viewer>
</div>
<div class="controls">
<div class="transparent"></div>
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 86366c799a2..c3cfe54eb4d 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
@@ -16,11 +16,6 @@ export default {
type: String,
required: true,
},
- projectPath: {
- type: String,
- required: false,
- default: '',
- },
},
data() {
return {
@@ -117,16 +112,14 @@ export default {
'height': swipeMaxPixelHeight,
}"
class="swipe-frame">
- <div class="frame deleted">
- <image-viewer
- key="swipeOldImg"
- ref="swipeOldImg"
- :render-info="false"
- :path="oldPath"
- :project-path="projectPath"
- @imgLoaded="swipeOldImgLoaded"
- />
- </div>
+ <image-viewer
+ key="swipeOldImg"
+ ref="swipeOldImg"
+ :render-info="false"
+ :path="oldPath"
+ class="frame deleted"
+ @imgLoaded="swipeOldImgLoaded"
+ />
<div
ref="swipeWrap"
:style="{
@@ -134,15 +127,19 @@ export default {
'height': swipeMaxPixelHeight,
}"
class="swipe-wrap">
- <div class="frame added">
- <image-viewer
- key="swipeNewImg"
- :render-info="false"
- :path="newPath"
- :project-path="projectPath"
- @imgLoaded="swipeNewImgLoaded"
- />
- </div>
+ <image-viewer
+ key="swipeNewImg"
+ :render-info="false"
+ :path="newPath"
+ class="frame added"
+ @imgLoaded="swipeNewImgLoaded"
+ >
+ <slot
+ slot="image-overlay"
+ name="image-overlay"
+ >
+ </slot>
+ </image-viewer>
</div>
<span
ref="swipeBar"
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue
index 9c19266ecdf..9806d65e940 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue
@@ -14,28 +14,29 @@ export default {
type: String,
required: true,
},
- projectPath: {
- type: String,
- required: false,
- default: '',
- },
},
};
</script>
<template>
- <div class="two-up view row">
- <div class="col-sm-6 frame deleted">
- <image-viewer
- :path="oldPath"
- :project-path="projectPath"
- />
- </div>
- <div class="col-sm-6 frame added">
- <image-viewer
- :path="newPath"
- :project-path="projectPath"
- />
- </div>
+ <div class="two-up view">
+ <image-viewer
+ :path="oldPath"
+ :render-info="true"
+ inner-css-classes="frame deleted"
+ class="wrap"
+ />
+ <image-viewer
+ :path="newPath"
+ :render-info="true"
+ :inner-css-classes="['frame', 'added']"
+ class="wrap"
+ >
+ <slot
+ slot="image-overlay"
+ name="image-overlay"
+ >
+ </slot>
+ </image-viewer>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue
index 1af85283277..d7f24c1afc5 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue
@@ -8,9 +8,6 @@ import { diffModes, imageViewMode } from '../constants';
export default {
components: {
ImageViewer,
- TwoUpViewer,
- SwipeViewer,
- OnionSkinViewer,
},
props: {
diffMode: {
@@ -25,17 +22,35 @@ export default {
type: String,
required: true,
},
- projectPath: {
- type: String,
- required: false,
- default: '',
- },
},
data() {
return {
mode: imageViewMode.twoup,
};
},
+ computed: {
+ imageViewComponent() {
+ switch (this.mode) {
+ case imageViewMode.twoup:
+ return TwoUpViewer;
+ case imageViewMode.swipe:
+ return SwipeViewer;
+ case imageViewMode.onion:
+ return OnionSkinViewer;
+ default:
+ return undefined;
+ }
+ },
+ isNew() {
+ return this.diffMode === diffModes.new;
+ },
+ isRenamed() {
+ return this.diffMode === diffModes.renamed;
+ },
+ imagePath() {
+ return this.isNew || this.isRenamed ? this.newPath : this.oldPath;
+ },
+ },
methods: {
changeMode(newMode) {
this.mode = newMode;
@@ -52,15 +67,16 @@ export default {
v-if="diffMode === $options.diffModes.replaced"
class="diff-viewer">
<div class="image js-replaced-image">
- <two-up-viewer
- v-if="mode === $options.imageViewMode.twoup"
- v-bind="$props"/>
- <swipe-viewer
- v-else-if="mode === $options.imageViewMode.swipe"
- v-bind="$props"/>
- <onion-skin-viewer
- v-else-if="mode === $options.imageViewMode.onion"
- v-bind="$props"/>
+ <component
+ :is="imageViewComponent"
+ v-bind="$props"
+ >
+ <slot
+ slot="image-overlay"
+ name="image-overlay"
+ >
+ </slot>
+ </component>
</div>
<div class="view-modes">
<ul class="view-modes-menu">
@@ -87,23 +103,27 @@ export default {
</li>
</ul>
</div>
- <div class="note-container"></div>
- </div>
- <div
- v-else-if="diffMode === $options.diffModes.new"
- class="diff-viewer added">
- <image-viewer
- :path="newPath"
- :project-path="projectPath"
- />
</div>
<div
v-else
- class="diff-viewer deleted">
- <image-viewer
- :path="oldPath"
- :project-path="projectPath"
- />
+ class="diff-viewer"
+ >
+ <div class="image">
+ <image-viewer
+ :path="imagePath"
+ :inner-css-classes="['frame', {
+ 'added': isNew,
+ 'deleted': diffMode === $options.diffModes.deleted
+ }]"
+ >
+ <slot
+ v-if="isNew || isRenamed"
+ slot="image-overlay"
+ name="image-overlay"
+ >
+ </slot>
+ </image-viewer>
+ </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
index 31087017968..0e194eaaed5 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
@@ -1,7 +1,11 @@
<script>
import { __ } from '~/locale';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
+ components: {
+ GlLoadingIcon,
+ },
props: {
isDisabled: {
type: Boolean,
diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue
index 408f7d7965f..03818be6a69 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/file_icon.vue
@@ -1,4 +1,5 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import getIconForFile from './file_icon/file_icon_map';
import icon from '../../vue_shared/components/icon.vue';
@@ -17,6 +18,7 @@ import icon from '../../vue_shared/components/icon.vue';
export default {
components: {
icon,
+ GlLoadingIcon,
},
props: {
fileName: {
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue b/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue
index 460fa6ad72e..388a2f4ca36 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue
@@ -56,12 +56,14 @@ export default {
filteredResults() {
if (this.filter !== '') {
return this.items.filter(
- item => item[this.filterKey] && item[this.filterKey].toLowerCase().includes(this.filter.toLowerCase()),
+ item =>
+ item[this.filterKey] &&
+ item[this.filterKey].toLowerCase().includes(this.filter.toLowerCase()),
);
}
return this.items.slice(0, this.visibleItems);
- }
+ },
},
mounted() {
/**
diff --git a/app/assets/javascripts/vue_shared/components/gl_countdown.vue b/app/assets/javascripts/vue_shared/components/gl_countdown.vue
index 9327a2a4a6c..a35986b2d03 100644
--- a/app/assets/javascripts/vue_shared/components/gl_countdown.vue
+++ b/app/assets/javascripts/vue_shared/components/gl_countdown.vue
@@ -1,10 +1,14 @@
<script>
import { calculateRemainingMilliseconds, formatTime } from '~/lib/utils/datetime_utility';
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
/**
* Counts down to a given end date.
*/
export default {
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
endDateString: {
type: String,
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
index aee88cae48d..88e95c33b9b 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -1,8 +1,9 @@
<script>
+import { GlTooltipDirective, GlLink, GlButton } from '@gitlab-org/gitlab-ui';
import CiIconBadge from './ci_badge_link.vue';
import TimeagoTooltip from './time_ago_tooltip.vue';
-import tooltip from '../directives/tooltip';
import UserAvatarImage from './user_avatar/user_avatar_image.vue';
+import LoadingButton from '~/vue_shared/components/loading_button.vue';
/**
* Renders header component for job and pipeline page based on UI mockups
@@ -16,9 +17,12 @@ export default {
CiIconBadge,
TimeagoTooltip,
UserAvatarImage,
+ GlLink,
+ GlButton,
+ LoadingButton,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
status: {
@@ -98,8 +102,8 @@ export default {
by
<template v-if="user">
- <a
- v-tooltip
+ <gl-link
+ v-gl-tooltip
:href="user.path"
:title="user.email"
class="js-user-link commit-committer-link"
@@ -113,7 +117,7 @@ export default {
/>
{{ user.name }}
- </a>
+ </gl-link>
<span
v-if="user.status_tooltip_html"
v-html="user.status_tooltip_html"></span>
@@ -127,16 +131,16 @@ export default {
<template
v-for="(action, i) in actions"
>
- <a
+ <gl-link
v-if="action.type === 'link'"
:key="i"
:href="action.path"
:class="action.cssClass"
>
{{ action.label }}
- </a>
+ </gl-link>
- <a
+ <gl-link
v-else-if="action.type === 'ujs-link'"
:key="i"
:href="action.path"
@@ -145,31 +149,24 @@ export default {
rel="nofollow"
>
{{ action.label }}
- </a>
+ </gl-link>
- <button
+ <loading-button
v-else-if="action.type === 'button'"
:key="i"
+ :loading="action.isLoading"
:disabled="action.isLoading"
:class="action.cssClass"
- type="button"
+ container-class="d-inline"
+ :label="action.label"
@click="onClickAction(action)"
- >
- {{ action.label }}
- <i
- v-show="action.isLoading"
- class="fa fa-spin fa-spinner"
- aria-hidden="true"
- >
- </i>
- </button>
+ />
</template>
</section>
- <button
+ <gl-button
v-if="hasSidebarButton"
id="toggleSidebar"
- type="button"
- class="btn btn-default d-block d-sm-none
+ class="d-block d-sm-none
sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header"
@click="onClickSidebarButton"
>
@@ -179,6 +176,6 @@ sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header"
aria-labelledby="toggleSidebar"
>
</i>
- </button>
+ </gl-button>
</header>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue
index 26f9d5ddc91..a25841fc02f 100644
--- a/app/assets/javascripts/vue_shared/components/icon.vue
+++ b/app/assets/javascripts/vue_shared/components/icon.vue
@@ -1,6 +1,6 @@
<script>
// only allow classes in images.scss e.g. s12
-const validSizes = [8, 10, 12, 16, 18, 24, 32, 48, 72];
+const validSizes = [8, 10, 12, 14, 16, 18, 24, 32, 48, 72];
let iconValidator = () => true;
/*
@@ -8,7 +8,7 @@ let iconValidator = () => true;
*/
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line global-require
- const data = require('@gitlab-org/gitlab-svgs/dist/icons.json');
+ const data = require('@gitlab/svgs/dist/icons.json');
const { icons } = data;
iconValidator = value => {
if (icons.includes(value)) {
diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue
index f9b7fd5b1f9..69d7e5c46f5 100644
--- a/app/assets/javascripts/vue_shared/components/loading_button.vue
+++ b/app/assets/javascripts/vue_shared/components/loading_button.vue
@@ -1,4 +1,5 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
/* eslint-disable vue/require-default-prop */
/* This is a re-usable vue component for rendering a button
that will probably be sending off ajax requests and need
@@ -18,6 +19,9 @@
*/
export default {
+ components: {
+ GlLoadingIcon,
+ },
props: {
loading: {
type: Boolean,
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 3ddb39730c4..27e3f314dd3 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -1,17 +1,17 @@
<script>
import $ from 'jquery';
-import Tooltip from '../../directives/tooltip';
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import ToolbarButton from './toolbar_button.vue';
import Icon from '../icon.vue';
export default {
- directives: {
- Tooltip,
- },
components: {
ToolbarButton,
Icon,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
previewMarkdown: {
type: Boolean,
@@ -147,7 +147,7 @@ export default {
icon="table"
/>
<button
- v-tooltip
+ v-gl-tooltip
aria-label="Go full screen"
class="toolbar-btn toolbar-fullscreen-btn js-zen-enter"
data-container="body"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
index 3e89e1c1e75..91d0bbfc21c 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
@@ -1,13 +1,13 @@
<script>
-import tooltip from '../../directives/tooltip';
-import icon from '../icon.vue';
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
+import Icon from '../icon.vue';
export default {
components: {
- icon,
+ Icon,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
buttonTitle: {
@@ -43,7 +43,7 @@ export default {
<template>
<button
- v-tooltip
+ v-gl-tooltip
:data-md-tag="tag"
:data-md-select="tagSelect"
:data-md-block="tagBlock"
diff --git a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
index f56414c3c63..8cb72afcdc0 100644
--- a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
@@ -10,7 +10,7 @@ export default {
</script>
<template>
- <li class="timeline-entry note">
+ <li class="timeline-entry note note-wrapper">
<div class="timeline-entry-inner">
<div class="timeline-icon">
</div>
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 de3c7a80365..6a44e6a29ed 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -76,7 +76,7 @@ export default {
<li
:id="noteAnchorId"
:class="{ target: isTargetNote }"
- class="note system-note timeline-entry">
+ class="note system-note timeline-entry note-wrapper">
<div class="timeline-entry-inner">
<div
class="timeline-icon"
diff --git a/app/assets/javascripts/vue_shared/components/pagination_links.vue b/app/assets/javascripts/vue_shared/components/pagination_links.vue
index 1f2a679c145..89dcf049f6e 100644
--- a/app/assets/javascripts/vue_shared/components/pagination_links.vue
+++ b/app/assets/javascripts/vue_shared/components/pagination_links.vue
@@ -1,7 +1,11 @@
<script>
+import { GlPagination } from '@gitlab-org/gitlab-ui';
import { s__ } from '../../locale';
export default {
+ components: {
+ GlPagination,
+ },
props: {
change: {
type: Function,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue
index 11fac3bb12c..5841db52704 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue
@@ -1,39 +1,39 @@
<script>
- import tooltip from '~/vue_shared/directives/tooltip';
+import tooltip from '~/vue_shared/directives/tooltip';
- export default {
- name: 'CollapsedCalendarIcon',
- directives: {
- tooltip,
+export default {
+ name: 'CollapsedCalendarIcon',
+ directives: {
+ tooltip,
+ },
+ props: {
+ containerClass: {
+ type: String,
+ required: false,
+ default: '',
},
- props: {
- containerClass: {
- type: String,
- required: false,
- default: '',
- },
- text: {
- type: String,
- required: false,
- default: '',
- },
- showIcon: {
- type: Boolean,
- required: false,
- default: true,
- },
- tooltipText: {
- type: String,
- required: false,
- default: '',
- },
+ text: {
+ type: String,
+ required: false,
+ default: '',
},
- methods: {
- click() {
- this.$emit('click');
- },
+ showIcon: {
+ type: Boolean,
+ required: false,
+ default: true,
},
- };
+ tooltipText: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ methods: {
+ click() {
+ this.$emit('click');
+ },
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue
index 6e7194ccc9e..174c29809ac 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue
@@ -1,84 +1,79 @@
<script>
- import { __ } from '~/locale';
- import timeagoMixin from '~/vue_shared/mixins/timeago';
- import { dateInWords, timeFor } from '~/lib/utils/datetime_utility';
- import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
+import { __ } from '~/locale';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+import { dateInWords, timeFor } from '~/lib/utils/datetime_utility';
+import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
- export default {
- name: 'SidebarCollapsedGroupedDatePicker',
- components: {
- collapsedCalendarIcon,
+export default {
+ name: 'SidebarCollapsedGroupedDatePicker',
+ components: {
+ collapsedCalendarIcon,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ collapsed: {
+ type: Boolean,
+ required: false,
+ default: true,
},
- mixins: [
- timeagoMixin,
- ],
- props: {
- collapsed: {
- type: Boolean,
- required: false,
- default: true,
- },
- minDate: {
- type: Date,
- required: false,
- default: null,
- },
- maxDate: {
- type: Date,
- required: false,
- default: null,
- },
- disableClickableIcons: {
- type: Boolean,
- required: false,
- default: false,
- },
+ minDate: {
+ type: Date,
+ required: false,
+ default: null,
},
- computed: {
- hasMinAndMaxDates() {
- return this.minDate && this.maxDate;
- },
- hasNoMinAndMaxDates() {
- return !this.minDate && !this.maxDate;
- },
- showMinDateBlock() {
- return this.minDate || this.hasNoMinAndMaxDates;
- },
- showFromText() {
- return !this.maxDate && this.minDate;
- },
- iconClass() {
- const disabledClass = this.disableClickableIcons ? 'disabled' : '';
- return `sidebar-collapsed-icon calendar-icon ${disabledClass}`;
- },
+ maxDate: {
+ type: Date,
+ required: false,
+ default: null,
},
- methods: {
- toggleSidebar() {
- this.$emit('toggleCollapse');
- },
- dateText(dateType = 'min') {
- const date = this[`${dateType}Date`];
- const dateWords = dateInWords(date, true);
- const parsedDateWords = dateWords ? dateWords.replace(',', '') : dateWords;
+ disableClickableIcons: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ hasMinAndMaxDates() {
+ return this.minDate && this.maxDate;
+ },
+ hasNoMinAndMaxDates() {
+ return !this.minDate && !this.maxDate;
+ },
+ showMinDateBlock() {
+ return this.minDate || this.hasNoMinAndMaxDates;
+ },
+ showFromText() {
+ return !this.maxDate && this.minDate;
+ },
+ iconClass() {
+ const disabledClass = this.disableClickableIcons ? 'disabled' : '';
+ return `sidebar-collapsed-icon calendar-icon ${disabledClass}`;
+ },
+ },
+ methods: {
+ toggleSidebar() {
+ this.$emit('toggleCollapse');
+ },
+ dateText(dateType = 'min') {
+ const date = this[`${dateType}Date`];
+ const dateWords = dateInWords(date, true);
+ const parsedDateWords = dateWords ? dateWords.replace(',', '') : dateWords;
- return date ? parsedDateWords : __('None');
- },
- tooltipText(dateType = 'min') {
- const defaultText = dateType === 'min' ? __('Start date') : __('Due date');
- const date = this[`${dateType}Date`];
- const timeAgo = dateType === 'min' ? this.timeFormated(date) : timeFor(date);
- const dateText = date ? [
- this.dateText(dateType),
- `(${timeAgo})`,
- ].join(' ') : '';
+ return date ? parsedDateWords : __('None');
+ },
+ tooltipText(dateType = 'min') {
+ const defaultText = dateType === 'min' ? __('Start date') : __('Due date');
+ const date = this[`${dateType}Date`];
+ const timeAgo = dateType === 'min' ? this.timeFormated(date) : timeFor(date);
+ const dateText = date ? [this.dateText(dateType), `(${timeAgo})`].join(' ') : '';
- if (date) {
- return [defaultText, dateText].join('<br />');
- }
- return __('Start and due date');
- },
+ if (date) {
+ return [defaultText, dateText].join('<br />');
+ }
+ return __('Start and due date');
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
index 500586302cf..5b12bb6b59e 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
@@ -1,4 +1,5 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import datePicker from '../pikaday.vue';
import toggleSidebar from './toggle_sidebar.vue';
import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
@@ -10,6 +11,7 @@ export default {
datePicker,
toggleSidebar,
collapsedCalendarIcon,
+ GlLoadingIcon,
},
props: {
blockClass: {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
index 3df286de129..e50d612ce36 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
@@ -4,6 +4,7 @@ import { __ } from '~/locale';
import LabelsSelect from '~/labels_select';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import DropdownTitle from './dropdown_title.vue';
import DropdownValue from './dropdown_value.vue';
import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
@@ -24,6 +25,7 @@ export default {
DropdownSearchInput,
DropdownFooter,
DropdownCreateLabel,
+ GlLoadingIcon,
},
props: {
showCreate: {
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 af297f3c408..0d5fc07e6e3 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,7 +14,10 @@ export default {
},
computed: {
labelsList() {
- const labelsString = this.labels.slice(0, 5).map(label => label.title).join(', ');
+ const labelsString = this.labels
+ .slice(0, 5)
+ .map(label => label.title)
+ .join(', ');
if (this.labels.length > 5) {
return sprintf(s__('LabelSelect|%{labelsString}, and %{remainingLabelCount} more'), {
diff --git a/app/assets/javascripts/vue_shared/components/smart_virtual_list.vue b/app/assets/javascripts/vue_shared/components/smart_virtual_list.vue
new file mode 100644
index 00000000000..63034a45f77
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/smart_virtual_list.vue
@@ -0,0 +1,42 @@
+<script>
+import VirtualList from 'vue-virtual-scroll-list';
+
+export default {
+ name: 'SmartVirtualList',
+ components: { VirtualList },
+ props: {
+ size: { type: Number, required: true },
+ length: { type: Number, required: true },
+ remain: { type: Number, required: true },
+ rtag: { type: String, default: 'div' },
+ wtag: { type: String, default: 'div' },
+ wclass: { type: String, default: null },
+ },
+};
+</script>
+<template>
+ <virtual-list
+ v-if="length > remain"
+ v-bind="$attrs"
+ :size="remain"
+ :remain="remain"
+ :rtag="rtag"
+ :wtag="wtag"
+ :wclass="wclass"
+ class="js-virtual-list"
+ >
+ <slot></slot>
+ </virtual-list>
+ <component
+ :is="rtag"
+ v-else
+ class="js-plain-element"
+ >
+ <component
+ :is="wtag"
+ :class="wclass"
+ >
+ <slot></slot>
+ </component>
+ </component>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/toggle_button.vue b/app/assets/javascripts/vue_shared/components/toggle_button.vue
index 4e9289cbed8..e7cb5cfac12 100644
--- a/app/assets/javascripts/vue_shared/components/toggle_button.vue
+++ b/app/assets/javascripts/vue_shared/components/toggle_button.vue
@@ -1,4 +1,5 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import { s__ } from '../../locale';
import icon from './icon.vue';
@@ -10,6 +11,7 @@ const LABEL_OFF = s__('ToggleButton|Toggle Status: OFF');
export default {
components: {
icon,
+ GlLoadingIcon,
},
model: {
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
index 7737b9f2697..4cfb1ded0a9 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
@@ -15,14 +15,14 @@
*/
+import { GlTooltip } from '@gitlab-org/gitlab-ui';
import defaultAvatarUrl from 'images/no_avatar.png';
import { placeholderImage } from '../../../lazy_loader';
-import tooltip from '../../directives/tooltip';
export default {
name: 'UserAvatarImage',
- directives: {
- tooltip,
+ components: {
+ GlTooltip,
},
props: {
lazy: {
@@ -73,9 +73,6 @@ export default {
resultantSrcAttribute() {
return this.lazy ? placeholderImage : this.sanitizedSource;
},
- tooltipContainer() {
- return this.tooltipText ? 'body' : null;
- },
avatarSizeClass() {
return `s${this.size}`;
},
@@ -84,22 +81,30 @@ export default {
</script>
<template>
- <img
- v-tooltip
- :class="{
- lazy: lazy,
- [avatarSizeClass]: true,
- [cssClasses]: true
- }"
- :src="resultantSrcAttribute"
- :width="size"
- :height="size"
- :alt="imgAlt"
- :data-src="sanitizedSource"
- :data-container="tooltipContainer"
- :data-placement="tooltipPlacement"
- :title="tooltipText"
- class="avatar"
- data-boundary="window"
- />
+ <span>
+ <img
+ ref="userAvatarImage"
+ :class="{
+ lazy: lazy,
+ [avatarSizeClass]: true,
+ [cssClasses]: true
+ }"
+ :src="resultantSrcAttribute"
+ :width="size"
+ :height="size"
+ :alt="imgAlt"
+ :data-src="sanitizedSource"
+ class="avatar"
+ />
+ <gl-tooltip
+ :target="() => $refs.userAvatarImage"
+ :placement="tooltipPlacement"
+ boundary="window"
+ class="js-user-avatar-image-toolip"
+ >
+ <slot>
+ {{ tooltipText }}
+ </slot>
+ </gl-tooltip>
+ </span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
index 86c7498a092..351a639c6e8 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
@@ -17,9 +17,8 @@
*/
-import { GlLink } from '@gitlab-org/gitlab-ui';
+import { GlLink, GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import userAvatarImage from './user_avatar_image.vue';
-import tooltip from '../../directives/tooltip';
export default {
name: 'UserAvatarLink',
@@ -28,7 +27,7 @@ export default {
userAvatarImage,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
linkHref: {
@@ -94,11 +93,14 @@ export default {
:size="imgSize"
:tooltip-text="avatarTooltipText"
:tooltip-placement="tooltipPlacement"
- /><span
+ >
+ <slot></slot>
+ </user-avatar-image><span
v-if="shouldShowUsername"
- v-tooltip
+ v-gl-tooltip
:title="tooltipText"
:tooltip-placement="tooltipPlacement"
- >{{ username }}</span>
+ class="js-user-avatar-link-username"
+ >{{ username }}</span><slot name="avatar-badge"></slot>
</gl-link>
</template>
diff --git a/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js b/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js
index 67a1632269e..f9e3f3df0cc 100644
--- a/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js
+++ b/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js
@@ -14,7 +14,14 @@ export default {
onChangePage(page) {
/* URLS parameters are strings, we need to parse to match types */
- this.updateContent({ scope: this.scope, page: Number(page).toString() });
+ const params = {
+ page: Number(page).toString(),
+ };
+
+ if (this.scope) {
+ params.scope = this.scope;
+ }
+ this.updateContent(params);
},
updateInternalState(parameters) {
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index 702276780e9..7a95db5976d 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -148,10 +148,7 @@
.award-control-icon svg {
background: $award-emoji-positive-add-bg;
-
- path {
- fill: $award-emoji-positive-add-lines;
- }
+ fill: $award-emoji-positive-add-lines;
}
.award-control-icon-neutral {
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index f26b1fddae5..43b7c26b272 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -348,6 +348,7 @@
@include media-breakpoint-down(xs) {
width: 100%;
+ margin: $btn-side-margin 0;
}
}
}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index c4296c7a88a..219fd99b097 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -207,6 +207,10 @@
@include btn-with-margin;
}
+ &.btn-icon {
+ color: $gl-gray-700;
+ }
+
.fa-caret-down,
.fa-chevron-down {
margin-left: 5px;
@@ -218,6 +222,25 @@
}
}
+ &.btn-text-field {
+ width: 100%;
+ text-align: left;
+ padding: 6px 16px;
+ border-color: $border-color;
+ color: $gray-darkest;
+ background-color: $gray-light;
+
+ &:hover,
+ &:active,
+ &:focus {
+ cursor: text;
+ box-shadow: none;
+ border-color: lighten($blue-300, 20%);
+ color: $gray-darkest;
+ background-color: $gray-light;
+ }
+ }
+
&.dot-highlight::after {
content: '';
background-color: $blue-500;
@@ -335,25 +358,6 @@
}
}
-.btn-text-field {
- width: 100%;
- text-align: left;
- padding: 6px 16px;
- border-color: $border-color;
- color: $gray-darkest;
- background-color: $gray-light;
-
- &:hover,
- &:active,
- &:focus {
- cursor: text;
- box-shadow: none;
- border-color: lighten($blue-300, 20%);
- color: $gray-darkest;
- background-color: $gray-light;
- }
-}
-
.btn-build {
margin-left: 10px;
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index fa753b13e5f..626c8f92d1d 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -33,6 +33,11 @@
color: $brand-danger;
}
+.text-danger-muted,
+.text-danger-muted:hover {
+ color: $red-300;
+}
+
.text-warning,
.text-warning:hover {
color: $brand-warning;
@@ -345,6 +350,7 @@ img.emoji {
/** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; }
.prepend-top-2 { margin-top: 2px; }
+.prepend-top-4 { margin-top: $gl-padding-4; }
.prepend-top-5 { margin-top: 5px; }
.prepend-top-8 { margin-top: $grid-size; }
.prepend-top-10 { margin-top: 10px; }
@@ -365,6 +371,7 @@ img.emoji {
.append-right-default { margin-right: $gl-padding; }
.append-right-20 { margin-right: 20px; }
.append-bottom-0 { margin-bottom: 0; }
+.append-bottom-4 { margin-bottom: $gl-padding-4; }
.append-bottom-5 { margin-bottom: 5px; }
.append-bottom-8 { margin-bottom: $grid-size; }
.append-bottom-10 { margin-bottom: 10px; }
diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss
index 2e7f25d975e..6f103e4e89a 100644
--- a/app/assets/stylesheets/framework/contextual_sidebar.scss
+++ b/app/assets/stylesheets/framework/contextual_sidebar.scss
@@ -322,15 +322,15 @@
width: $contextual-sidebar-width - 1px;
transition: width $sidebar-transition-duration;
position: fixed;
+ height: $toggle-sidebar-height;
bottom: 0;
- padding: $gl-padding;
+ padding: 0 $gl-padding;
background-color: $gray-light;
border: 0;
border-top: 1px solid $border-color;
color: $gl-text-color-secondary;
display: flex;
align-items: center;
- line-height: 1;
svg {
margin-right: 8px;
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index cdfad30e7ca..dca89981d81 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -158,7 +158,7 @@
color: $gl-text-color;
outline: 0;
- // make sure the text color is not overriden
+ // make sure the text color is not overridden
&.text-danger {
color: $brand-danger;
}
@@ -184,7 +184,7 @@
text-align: left;
width: 100%;
- // make sure the text color is not overriden
+ // make sure the text color is not overridden
&.text-danger {
color: $brand-danger;
}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 53f198b47c6..6bdcb20210b 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -36,7 +36,6 @@
text-align: left;
padding: 10px $gl-padding;
word-wrap: break-word;
- border-radius: $border-radius-default $border-radius-default 0 0;
&.file-title-clear {
padding-left: 0;
diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss
index 1e93bf2b751..a20920e2503 100644
--- a/app/assets/stylesheets/framework/images.scss
+++ b/app/assets/stylesheets/framework/images.scss
@@ -39,7 +39,7 @@
svg {
fill: currentColor;
- $svg-sizes: 8 10 12 16 18 24 32 48 72;
+ $svg-sizes: 8 10 12 14 16 18 24 32 48 72;
@each $svg-size in $svg-sizes {
&.s#{$svg-size} {
@include svg-size(#{$svg-size}px);
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index c030d75f5a4..9837b1a6bd0 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -291,7 +291,7 @@
/*
* Mixin that handles the position of the controls placed on the top bar
*/
-@mixin build-controllers($control-font-size, $flex-direction, $with-grow, $flex-grow-size) {
+@mixin build-controllers($control-font-size, $flex-direction, $with-grow, $flex-grow-size, $svg-display: 'block', $svg-top: '2px') {
display: flex;
font-size: $control-font-size;
justify-content: $flex-direction;
@@ -304,8 +304,9 @@
svg {
width: 15px;
height: 15px;
- display: block;
+ display: $svg-display;
fill: $gl-text-color;
+ top: $svg-top;
}
.controllers-buttons {
diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
index f47dfe1b563..de9e7c37695 100644
--- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss
+++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
@@ -1,6 +1,6 @@
// For tabbed navigation links, scrolling tabs, etc. For all top/main navigation,
// please check nav.scss
-.nav-links {
+.nav-links:not(.quick-links) {
display: flex;
padding: 0;
margin: 0;
@@ -106,7 +106,7 @@
display: inline-block;
float: right;
text-align: right;
- padding: 11px 0;
+ padding: $gl-padding-8 0;
margin-bottom: 0;
> .btn,
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 381c0290d32..7f0edd88dfb 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -243,6 +243,8 @@
.user-result {
min-height: 24px;
+ display: flex;
+ align-items: center;
.user-image {
float: left;
diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss
index 339388392df..6954e6599b1 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -147,3 +147,9 @@ table {
}
}
}
+
+.top-area + .content-list {
+ th {
+ border-top: 0;
+ }
+}
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index dfb145debe7..4a311da1675 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -1,7 +1,7 @@
.timeline {
- @include basic-list;
margin: 0;
padding: 0;
+ list-style: none;
&::before {
@include notes-media('max', map-get($grid-breakpoints, sm)) {
@@ -26,10 +26,8 @@
}
.timeline-entry {
- border-color: $white-normal;
color: $gl-text-color;
- border-bottom: 1px solid $border-white-light;
- background: $white-light;
+ background-color: $white-light;
.timeline-entry-inner {
position: relative;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index ad66a0365ed..f4540146a25 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -10,6 +10,7 @@ $sidebar-breakpoint: 1024px;
$default-transition-duration: 0.15s;
$contextual-sidebar-width: 220px;
$contextual-sidebar-collapsed-width: 50px;
+$toggle-sidebar-height: 48px;
/*
* Color schema
@@ -194,6 +195,7 @@ $well-light-text-color: #5b6169;
* Text
*/
$gl-font-size: 14px;
+$gl-font-size-xs: 11px;
$gl-font-size-small: 12px;
$gl-font-weight-normal: 400;
$gl-font-weight-bold: 600;
@@ -268,6 +270,7 @@ $flash-height: 52px;
$context-header-height: 60px;
$breadcrumb-min-height: 48px;
$project-title-row-height: 24px;
+$gl-line-height: 16px;
/*
* Common component specific colors
@@ -438,7 +441,7 @@ $ci-skipped-color: #888;
* Boards
*/
$issue-boards-font-size: 14px;
-$issue-boards-card-shadow: rgba(186, 186, 186, 0.5);
+$issue-boards-card-shadow: rgba(0, 0, 0, 0.1);
/*
The following heights are used in boards.scss and are used for calculation of the board height.
They probably should be derived in a smarter way.
@@ -638,3 +641,8 @@ Modals
$modal-body-height: 134px;
$priority-label-empty-state-width: 114px;
+
+/*
+Issues Analytics
+*/
+$issues-analytics-popover-boarder-color: rgba(0, 0, 0, 0.15);
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 54fbd40cece..c6074eb9df4 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -90,20 +90,14 @@
}
.with-performance-bar & {
- height: calc(
- 100vh - #{$issue-board-list-difference-xs} - #{$performance-bar-height}
- );
+ height: calc(100vh - #{$issue-board-list-difference-xs} - #{$performance-bar-height});
@include media-breakpoint-only(sm) {
- height: calc(
- 100vh - #{$issue-board-list-difference-sm} - #{$performance-bar-height}
- );
+ height: calc(100vh - #{$issue-board-list-difference-sm} - #{$performance-bar-height});
}
@include media-breakpoint-up(md) {
- height: calc(
- 100vh - #{$issue-board-list-difference-md} - #{$performance-bar-height}
- );
+ height: calc(100vh - #{$issue-board-list-difference-md} - #{$performance-bar-height});
}
}
}
@@ -271,7 +265,7 @@
height: 100%;
width: 100%;
margin-bottom: 0;
- padding: 5px;
+ padding: $gl-padding-4;
list-style: none;
overflow-y: auto;
overflow-x: hidden;
@@ -284,14 +278,16 @@
.board-card {
position: relative;
- padding: 11px 10px 11px $gl-padding;
+ padding: $gl-padding;
background: $white-light;
border-radius: $border-radius-default;
+ border: 1px solid $theme-gray-200;
box-shadow: 0 1px 2px $issue-boards-card-shadow;
list-style: none;
+ line-height: $gl-padding;
&:not(:last-child) {
- margin-bottom: 5px;
+ margin-bottom: $gl-padding-8;
}
&.is-active,
@@ -302,113 +298,120 @@
.badge {
border: 0;
outline: 0;
+
+ &:hover {
+ text-decoration: underline;
+ }
+
+ @include media-breakpoint-down(lg) {
+ font-size: $gl-font-size-xs;
+ padding-left: $gl-padding-4;
+ padding-right: $gl-padding-4;
+ font-weight: $gl-font-weight-bold;
+ }
+ }
+
+ svg {
+ vertical-align: top;
}
.confidential-icon {
- vertical-align: text-top;
- margin-right: 5px;
+ color: $orange-600;
+ cursor: help;
+ }
+
+ @include media-breakpoint-down(md) {
+ padding: $gl-padding-8;
}
}
.board-card-title {
@include overflow-break-word();
- margin: 0 30px 0 0;
font-size: 1em;
- line-height: inherit;
a {
color: $gl-text-color;
- margin-right: 2px;
+ }
+
+ @include media-breakpoint-down(md) {
+ font-size: $label-font-size;
}
}
.board-card-header {
display: flex;
- min-height: 20px;
-
- .board-card-assignee {
- display: flex;
- justify-content: flex-end;
- position: absolute;
- right: 15px;
- height: 20px;
- width: 20px;
+}
- .avatar-counter {
- display: none;
- vertical-align: middle;
- min-width: 20px;
- line-height: 19px;
- height: 20px;
- padding-left: 2px;
- padding-right: 2px;
- border-radius: 2em;
- }
+.board-card-assignee {
+ display: flex;
+ margin-top: -$gl-padding-4;
+ margin-bottom: -$gl-padding-4;
+
+ .avatar-counter {
+ vertical-align: middle;
+ line-height: $gl-padding-24;
+ min-width: $gl-padding-24;
+ height: $gl-padding-24;
+ border-radius: $gl-padding-24;
+ background-color: $gl-text-color-tertiary;
+ font-size: $gl-font-size-xs;
+ cursor: help;
+ font-weight: $gl-font-weight-bold;
+ margin-left: -$gl-padding-4;
+ border: 0;
+ padding: 0 $gl-padding-4;
- img {
- vertical-align: top;
+ @include media-breakpoint-down(md) {
+ min-width: auto;
+ height: $gl-padding;
+ border-radius: $gl-padding;
+ line-height: $gl-padding;
}
+ }
- a {
- position: relative;
- margin-left: -15px;
- }
+ img {
+ vertical-align: top;
+ }
- a:nth-child(1) {
- z-index: 3;
- }
+ .user-avatar-link:not(:only-child) {
+ margin-left: -$gl-padding-4;
- a:nth-child(2) {
+ &:nth-of-type(1) {
z-index: 2;
}
- a:nth-child(3) {
+ &:nth-of-type(2) {
z-index: 1;
}
+ }
- a:nth-child(4) {
- display: none;
- }
-
- &:hover {
- .avatar-counter {
- display: inline-block;
- }
-
- a {
- position: static;
- background-color: $white-light;
- transition: background-color 0s;
- margin-left: auto;
-
- &:nth-child(4) {
- display: block;
- }
+ .avatar {
+ margin: 0;
- &:first-child:not(:only-child) {
- box-shadow: -10px 0 10px 1px $white-light;
- }
- }
+ @include media-breakpoint-down(md) {
+ width: $gl-padding;
+ height: $gl-padding;
}
}
- .avatar {
- margin: 0;
+ @include media-breakpoint-down(md) {
+ margin-top: 0;
+ margin-bottom: 0;
}
}
-.board-card-footer {
- margin: 0 0 5px;
+.board-card-number {
+ font-size: $gl-font-size-xs;
+ color: $gl-text-color-secondary;
+ overflow: hidden;
- .badge {
- margin-top: 5px;
- margin-right: 6px;
+ @include media-breakpoint-up(md) {
+ font-size: $label-font-size;
}
}
-.board-card-number {
- font-size: 12px;
- color: $gl-text-color-secondary;
+.board-card-number-container {
+ overflow: hidden;
}
.issue-boards-search {
@@ -474,8 +477,7 @@
.right-sidebar.right-sidebar-expanded {
&.boards-sidebar-slide-enter-active,
&.boards-sidebar-slide-leave-active {
- transition: width $sidebar-transition-duration,
- padding $sidebar-transition-duration;
+ transition: width $sidebar-transition-duration, padding $sidebar-transition-duration;
}
&.boards-sidebar-slide-enter,
@@ -650,3 +652,36 @@
}
}
}
+
+.board-card-info {
+ color: $gl-text-color-secondary;
+ white-space: nowrap;
+ margin-right: $gl-padding-8;
+
+ &:not(.board-card-weight) {
+ cursor: help;
+ }
+
+ &.board-card-weight {
+ color: $gl-text-color;
+ cursor: pointer;
+
+ &:hover {
+ color: initial;
+ text-decoration: underline;
+ }
+ }
+
+ .board-card-info-icon {
+ color: $theme-gray-600;
+ margin-right: $gl-padding-4;
+ }
+
+ @include media-breakpoint-down(md) {
+ font-size: $label-font-size;
+ }
+}
+
+.board-issue-path.js-show-tooltip {
+ cursor: help;
+}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 31b258e56dd..81cb519883b 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -55,9 +55,29 @@
@include build-trace();
}
+ .archived-sticky {
+ top: $header-height;
+ border-radius: 2px 2px 0 0;
+ color: $orange-600;
+ background-color: $orange-100;
+ border: 1px solid $border-gray-normal;
+ border-bottom: 0;
+ padding: 3px 12px;
+ margin: auto;
+ align-items: center;
+
+ .with-performance-bar & {
+ top: $header-height + $performance-bar-height;
+ }
+ }
+
.top-bar {
@include build-trace-top-bar(35px);
+ &.has-archived-block {
+ top: $header-height + $performance-bar-height + 28px;
+ }
+
&.affix {
top: $header-height;
@@ -94,7 +114,7 @@
}
.controllers {
- @include build-controllers(15px, center, false, 0);
+ @include build-controllers(15px, center, false, 0, inline, 0);
}
}
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss
index 71a3fd544f2..ad12cd101b6 100644
--- a/app/assets/stylesheets/pages/clusters.scss
+++ b/app/assets/stylesheets/pages/clusters.scss
@@ -25,6 +25,12 @@
.cluster-application-row {
border-bottom: 1px solid $border-color;
padding: $gl-padding;
+
+ &:last-child {
+ border-bottom: 0;
+ border-bottom-left-radius: calc(#{$border-radius-default} - 1px);
+ border-bottom-right-radius: calc(#{$border-radius-default} - 1px);
+ }
}
}
@@ -73,6 +79,10 @@
padding: $gl-padding-top $gl-padding;
}
+ .card {
+ margin-bottom: $gl-vert-padding;
+ }
+
.empty-state .svg-content img {
width: 145px;
}
@@ -80,6 +90,31 @@
.top-area .nav-controls > .btn.btn-add-cluster {
margin-right: 0;
}
+
+ .clusters-table {
+ background-color: $gray-light;
+ padding: $gl-padding-8;
+ }
+
+ .badge-light {
+ background-color: $white-normal;
+ }
+
+ .gl-responsive-table-row {
+ padding: $gl-padding;
+ border: 0;
+
+ &.table-row-header {
+ background-color: none;
+ border: 0;
+ font-weight: bold;
+ color: $gl-gray-500;
+ }
+ }
+}
+
+.cluster-warning {
+ @include alert-variant(theme-color-level('warning', $alert-bg-level), theme-color-level('warning', $alert-border-level), theme-color-level('warning', $alert-color-level));
}
.gcp-signup-offer {
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 52c91266ff4..6d998fa1e07 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -59,6 +59,7 @@
margin: 0;
padding: 0;
table-layout: fixed;
+ border-radius: 0 0 $border-radius-default $border-radius-default;
.diff-line-num {
width: 50px;
@@ -421,21 +422,13 @@
.diff-file-container {
.frame.deleted {
- border: 0;
+ border: 1px solid $deleted;
background-color: inherit;
-
- .image_file img {
- border: 1px solid $deleted;
- }
}
.frame.added {
- border: 0;
+ border: 1px solid $added;
background-color: inherit;
-
- .image_file img {
- border: 1px solid $added;
- }
}
.swipe.view,
@@ -481,6 +474,11 @@
bottom: -25px;
}
}
+
+ .discussion-notes .discussion-notes {
+ margin-left: 0;
+ border-left: 0;
+ }
}
.file-content .diff-file {
@@ -804,7 +802,7 @@
// double jagged line divider
.discussion-notes + .discussion-notes::before,
- .discussion-notes + .discussion-form::before {
+ .diff-file-discussions + .discussion-form::before {
content: '';
position: relative;
display: block;
@@ -844,6 +842,13 @@
background-repeat: repeat;
}
+ .diff-file-discussions + .discussion-form::before {
+ width: auto;
+ margin-left: -16px;
+ margin-right: -16px;
+ margin-bottom: 16px;
+ }
+
.notes {
position: relative;
}
@@ -855,7 +860,7 @@
}
.diff-file .note-container > .new-note,
-.note-container .discussion-notes {
+.note-container .discussion-notes.diff-discussions {
margin-left: 100px;
border-left: 1px solid $white-normal;
}
@@ -870,11 +875,13 @@
}
}
-.files:not([data-can-create-note]) .frame {
+.files:not([data-can-create-note="true"]) .frame {
cursor: auto;
}
-.frame.click-to-comment {
+.frame,
+.frame.click-to-comment,
+.btn-transparent.image-diff-overlay-add-comment {
position: relative;
cursor: image-url('illustrations/image_comment_light_cursor.svg')
$image-comment-cursor-left-offset $image-comment-cursor-top-offset,
@@ -910,6 +917,7 @@
.frame .badge.badge-pill,
.image-diff-avatar-link .badge.badge-pill,
+.user-avatar-link .badge.badge-pill,
.notes > .badge.badge-pill {
position: absolute;
background-color: $blue-400;
@@ -944,7 +952,8 @@
}
}
-.image-diff-avatar-link {
+.image-diff-avatar-link,
+.user-avatar-link {
position: relative;
.badge.badge-pill,
@@ -1073,3 +1082,14 @@
top: 0;
}
}
+
+.image-diff-overlay,
+.image-diff-overlay-add-comment {
+ top: 0;
+ left: 0;
+
+ &:active,
+ &:focus {
+ outline: 0;
+ }
+}
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 19a36061c45..347fcad771a 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -44,11 +44,6 @@
margin: 0;
}
- .icon-play {
- height: 13px;
- width: 12px;
- }
-
.external-url,
.dropdown-new {
color: $gl-text-color-secondary;
@@ -366,7 +361,7 @@
}
.arrow-shadow {
- content: "";
+ content: '';
position: absolute;
width: 7px;
height: 7px;
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index a91d44805ee..618f23d81b1 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -4,41 +4,29 @@
*/
.event-item {
font-size: $gl-font-size;
- padding: $gl-padding-top 0 $gl-padding-top 40px;
+ padding: $gl-padding 0 $gl-padding 56px;
border-bottom: 1px solid $white-normal;
- color: $gl-text-color;
+ color: $gl-text-color-secondary;
position: relative;
-
- &.event-inline {
- .system-note-image {
- top: 20px;
- }
-
- .user-avatar {
- top: 14px;
- }
-
- .event-title,
- .event-item-timestamp {
- line-height: 40px;
- }
- }
-
- a {
- color: $gl-text-color;
- }
+ line-height: $gl-line-height;
.system-note-image {
position: absolute;
left: 0;
- top: 14px;
svg {
- width: 20px;
- height: 20px;
fill: $gl-text-color-secondary;
}
+ }
+
+ .system-note-image-inline {
+ svg {
+ fill: $gl-text-color-secondary;
+ }
+ }
+ .system-note-image,
+ .system-note-image-inline {
&.opened-icon,
&.created-icon {
svg {
@@ -53,16 +41,35 @@
&.accepted-icon svg {
fill: $blue-300;
}
+
+ &.commented-on-icon svg {
+ fill: $blue-600;
+ }
+ }
+
+ .event-user-info {
+ margin-bottom: $gl-padding-8;
+
+ .author_name {
+ a {
+ color: $gl-text-color;
+ font-weight: $gl-font-weight-bold;
+ }
+ }
}
.event-title {
- @include str-truncated(calc(100% - 174px));
- font-weight: $gl-font-weight-bold;
- color: $gl-text-color;
+ .event-type {
+ &::first-letter {
+ text-transform: capitalize;
+ }
+ }
}
.event-body {
+ margin-top: $gl-padding-8;
margin-right: 174px;
+ color: $gl-text-color;
.event-note {
word-wrap: break-word;
@@ -92,7 +99,7 @@
}
.note-image-attach {
- margin-top: 4px;
+ margin-top: $gl-padding-4;
margin-left: 0;
max-width: 200px;
float: none;
@@ -107,7 +114,6 @@
color: $gl-gray-500;
float: left;
font-size: $gl-font-size;
- line-height: 16px;
margin-right: 5px;
}
}
@@ -127,7 +133,9 @@
}
}
- &:last-child { border: 0; }
+ &:last-child {
+ border: 0;
+ }
.event_commits {
li {
@@ -154,7 +162,6 @@
.event-item-timestamp {
float: right;
- line-height: 22px;
}
}
@@ -177,10 +184,8 @@
.event-item {
padding-left: 0;
- &.event-inline {
- .event-title {
- line-height: 20px;
- }
+ .event-user-info {
+ margin-bottom: $gl-padding-4;
}
.event-title {
@@ -194,7 +199,8 @@
}
.event-body {
- margin: 0;
+ margin-top: $gl-padding-4;
+ margin-right: 0;
padding-left: 0;
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 00b06aea898..3aa79bf2466 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -47,12 +47,6 @@
@extend .fixed-width-container;
}
}
-
- .diffs {
- .mr-version-controls {
- @extend .fixed-width-container;
- }
- }
}
.issuable-details {
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index c60bb360a03..97b3f696139 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -176,8 +176,10 @@
background-color: $white-light;
}
-.discussion-form-container {
- padding: $gl-padding-top $gl-padding $gl-padding;
+table {
+ .discussion-form-container {
+ padding: $gl-padding-top $gl-padding $gl-padding;
+ }
}
.discussion-notes .disabled-comment {
@@ -239,6 +241,7 @@
.discussion-reply-holder {
background-color: $white-light;
padding: 10px 16px;
+ border-radius: 0 0 3px 3px;
&.is-replying {
padding-bottom: $gl-padding;
@@ -247,10 +250,15 @@
}
.discussion-with-resolve-btn {
+ @include media-breakpoint-up(sm) {
+ display: flex;
+ }
+
+
.discussion-actions {
display: table;
- .btn-default path {
+ svg {
fill: $gray-darkest;
}
@@ -270,6 +278,12 @@
.btn {
width: 100%;
}
+
+ .btn-text-field {
+ @include media-breakpoint-down(xs) {
+ margin-bottom: $gl-padding-8;
+ }
+ }
}
.discussion-notes-count {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index be535ade0a6..1f34537d856 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -1,26 +1,139 @@
-/**
- * Notes
- */
+$system-note-icon-size: 32px;
+$system-note-svg-size: 16px;
+$note-form-margin-left: 72px;
-@-webkit-keyframes targe3-note {
- from {
- background: $note-targe3-outside;
+@mixin vertical-line($left) {
+ &::before {
+ content: '';
+ border-left: 2px solid $theme-gray-100;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: $left;
}
+}
- 50% {
- background: $note-targe3-inside;
- }
+@mixin outline-comment() {
+ margin: $gl-padding;
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+}
+
+.note-wrapper {
+ padding: $gl-padding;
- to {
- background: $note-targe3-outside;
+ &.outlined {
+ @include outline-comment();
}
}
-ul.notes {
+.main-notes-list {
+ @include vertical-line(36px);
+}
+
+.notes {
display: block;
list-style: none;
margin: 0;
padding: 0;
+ position: relative;
+
+ &.timeline > .timeline-entry {
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+ margin: $gl-padding 0;
+
+ &.system-note,
+ &.note-form {
+ border: 0;
+ }
+
+ &.note-form {
+ margin-left: 0;
+
+ @include notes-media('min', map-get($grid-breakpoints, md)) {
+ margin-left: $note-form-margin-left;
+ }
+
+ .timeline-icon {
+ @include notes-media('min', map-get($grid-breakpoints, sm)) {
+ margin-left: -$note-icon-gutter-width;
+ }
+ }
+
+ .timeline-content {
+ margin-left: 0;
+ }
+ }
+
+ .notes_content {
+ border: 0;
+ border-top: 1px solid $border-color;
+ }
+ }
+
+ > .note-discussion {
+ .card {
+ border: 0;
+ }
+
+ li.note {
+ border-bottom: 1px solid $border-color;
+ }
+ }
+
+ .replies-toggle {
+ background-color: $gray-light;
+ padding: $gl-padding-8 $gl-padding;
+
+ .collapse-replies-btn:hover {
+ color: $blue-600;
+ }
+
+ &.expanded {
+ border-bottom: 1px solid $border-color;
+
+ span {
+ cursor: pointer;
+ }
+
+ svg {
+ position: relative;
+ top: 3px;
+ }
+ }
+
+ &.collapsed {
+ color: $gl-text-color-secondary;
+
+ svg {
+ float: left;
+ position: relative;
+ top: $gl-padding-4;
+ margin-right: $gl-padding-8;
+ cursor: pointer;
+ }
+
+ img {
+ margin: -2px 4px 0 0;
+ }
+
+ .author-link {
+ color: $gl-text-color;
+ }
+ }
+
+ .user-avatar-link {
+ &:last-child img {
+ margin-right: $gl-padding-8;
+ }
+ }
+
+ .btn-link {
+ border: 0;
+ vertical-align: baseline;
+ }
+ }
.note-created-ago,
.note-updated-at {
@@ -28,8 +141,6 @@ ul.notes {
}
.discussion-body {
- padding-top: 8px;
-
.card {
margin-bottom: 0;
}
@@ -46,21 +157,10 @@ ul.notes {
}
> li {
- // .timeline-entry
- padding: 0;
display: block;
position: relative;
border-bottom: 0;
- @include notes-media('min', map-get($grid-breakpoints, sm)) {
- padding-left: $note-icon-gutter-width;
- }
-
- .timeline-entry-inner {
- padding: $gl-padding $gl-btn-padding;
- border-bottom: 1px solid $white-normal;
- }
-
&:target,
&.target {
border-bottom: 1px solid $white-normal;
@@ -75,23 +175,10 @@ ul.notes {
}
}
- .timeline-icon {
- @include notes-media('min', map-get($grid-breakpoints, sm)) {
- margin-left: -$note-icon-gutter-width;
- }
- }
-
- .timeline-content {
- margin-left: $note-icon-gutter-width;
-
- @include notes-media('min', map-get($grid-breakpoints, sm)) {
- margin-left: 0;
- }
- }
-
&.being-posted {
pointer-events: none;
opacity: 0.5;
+ padding: $gl-padding;
.dummy-avatar {
background-color: $gl-gray-200;
@@ -104,12 +191,6 @@ ul.notes {
}
}
- &.note-discussion {
- .timeline-entry-inner {
- padding: $gl-padding 10px;
- }
- }
-
.editing-spinner {
display: none;
}
@@ -191,8 +272,9 @@ ul.notes {
}
.system-note {
- font-size: 14px;
- clear: both;
+ padding: 6px 21px;
+ margin: $gl-padding-24 0;
+ background-color: transparent;
.note-header-info {
padding-bottom: 0;
@@ -225,17 +307,21 @@ ul.notes {
.timeline-icon {
float: left;
-
- @include notes-media('min', map-get($grid-breakpoints, sm)) {
- margin-left: 0;
- width: auto;
- }
+ display: flex;
+ align-items: center;
+ background-color: $white-light;
+ width: $system-note-icon-size;
+ height: $system-note-icon-size;
+ border: 1px solid $border-color;
+ border-radius: $system-note-icon-size;
+ margin: -6px $gl-padding 0 0;
svg {
- width: 16px;
- height: 16px;
+ width: $system-note-svg-size;
+ height: $system-note-svg-size;
fill: $gray-darkest;
- margin-top: 2px;
+ display: block;
+ margin: 0 auto;
}
}
@@ -302,10 +388,18 @@ ul.notes {
.discussion-body .diff-file {
.file-title {
cursor: default;
+ line-height: 42px;
+ padding: 0 $gl-padding;
+ border-top: 1px solid $border-color;
+ border-radius: 0;
&:hover {
background-color: $gray-light;
}
+
+ .btn-clipboard {
+ top: 10px;
+ }
}
.line_content {
@@ -313,6 +407,24 @@ ul.notes {
}
}
+.tab-pane.notes {
+ .diff-file .notes .system-note {
+ margin: 0;
+ }
+}
+
+.tab-pane.diffs {
+ .system-note {
+ padding: 0 $gl-padding;
+ margin-left: 20px;
+ }
+
+ .notes > .note-discussion li.note.system-note {
+ border-bottom: 0;
+ padding: 0 $gl-padding;
+ }
+}
+
.diff-file {
.is-over {
.add-diff-note {
@@ -320,6 +432,23 @@ ul.notes {
}
}
+ .discussion-notes {
+ &:not(:first-child) {
+ border-top: 1px solid $white-normal;
+ margin-top: 20px;
+ }
+
+ &:not(:last-child) {
+ border-bottom: 1px solid $white-normal;
+ margin-bottom: 20px;
+ }
+
+ .system-note {
+ background-color: $white-light;
+ padding: $gl-padding;
+ }
+ }
+
// Merge request notes in diffs
// Diff is inline
.notes_content .note-header .note-headline-light {
@@ -335,7 +464,6 @@ ul.notes {
border-left: 0;
&.notes_content {
- background-color: $gray-light;
border-width: 1px 0;
padding: 0;
vertical-align: top;
@@ -349,18 +477,6 @@ ul.notes {
}
}
- .discussion-notes {
- &:not(:first-child) {
- border-top: 1px solid $white-normal;
- margin-top: 20px;
- }
-
- &:not(:last-child) {
- border-bottom: 1px solid $white-normal;
- margin-bottom: 20px;
- }
- }
-
.notes {
background-color: $white-light;
}
@@ -374,6 +490,47 @@ ul.notes {
}
}
+.diffs {
+ .discussion-notes {
+ margin-left: 0;
+ border-left: 0;
+
+ .notes {
+ position: relative;
+ @include vertical-line(52px);
+ }
+ }
+
+ .note-wrapper {
+ @include outline-comment();
+
+ &.system-note {
+ border: 0;
+ margin-left: 20px;
+ }
+ }
+
+ .discussion-reply-holder {
+ border-radius: 0 0 $border-radius-default $border-radius-default;
+ border-top: 1px solid $border-color;
+ position: relative;
+ }
+}
+
+.commit-diff {
+ .notes {
+ @include vertical-line(52px);
+ }
+
+ .notes_content {
+ background-color: $white-light;
+ }
+
+ .discussion-reply-holder {
+ border-top: 1px solid $border-color;
+ }
+}
+
.discussion-header,
.note-header-info {
a {
@@ -399,7 +556,17 @@ ul.notes {
}
.discussion-header {
- font-size: 14px;
+ min-height: 72px;
+
+ .note-header-info {
+ padding-bottom: 0;
+ }
+}
+
+.unresolved {
+ .note-header-info {
+ margin-top: $gl-padding-8;
+ }
}
.note-header {
@@ -409,7 +576,7 @@ ul.notes {
.note-header-info {
min-width: 0;
- padding-bottom: 8px;
+ padding-bottom: $gl-padding-8;
&.discussion {
padding-bottom: 0;
@@ -468,12 +635,20 @@ ul.notes {
.discussion-actions {
float: right;
- margin-left: 10px;
color: $gray-darkest;
+ @include media-breakpoint-down(xs) {
+ width: 100%;
+ margin: $gl-padding-8 0;
+ }
+
.btn-group > .discussion-next-btn {
margin-left: -1px;
}
+
+ svg {
+ height: 15px;
+ }
}
.note-actions {
@@ -585,19 +760,6 @@ ul.notes {
z-index: 10;
}
-.discussion-body,
-.diff-file {
- .notes .note {
- border-bottom: 1px solid $white-normal;
-
- .timeline-entry-inner {
- padding-left: $gl-padding;
- padding-right: $gl-padding;
- border-bottom: 0;
- }
- }
-}
-
.disabled-comment {
background-color: $gray-light;
border-radius: $border-radius-base;
@@ -634,7 +796,7 @@ ul.notes {
}
.btn {
- svg path {
+ svg {
fill: $gray-darkest;
}
@@ -659,7 +821,7 @@ ul.notes {
.line-resolve-all {
vertical-align: middle;
display: inline-block;
- padding: 5px 10px 6px;
+ padding: 6px 10px;
background-color: $gray-light;
border: 1px solid $border-color;
border-radius: $border-radius-default;
@@ -775,3 +937,23 @@ ul.notes {
}
}
}
+
+//This needs to be deleted when Snippet/Commit comments are convered to Vue
+// See https://gitlab.com/gitlab-org/gitlab-ce/issues/53918#note_117038785
+.unstyled-comments {
+
+ .discussion-header {
+ padding: $gl-padding;
+ border-bottom: 1px solid $border-color;
+ }
+
+ .note-wrapper.outlined {
+ margin: 0;
+ border: 0;
+ border-radius: 0;
+ }
+
+ .discussion-form-container {
+ padding: $gl-padding;
+ }
+}
diff --git a/app/assets/stylesheets/pages/pages.scss b/app/assets/stylesheets/pages/pages.scss
index fb42dee66d2..374227fe16a 100644
--- a/app/assets/stylesheets/pages/pages.scss
+++ b/app/assets/stylesheets/pages/pages.scss
@@ -1,7 +1,5 @@
.pages-domain-list {
&-item {
- position: relative;
- display: flex;
align-items: center;
.domain-status {
@@ -44,8 +42,9 @@
}
:first-child {
- border-bottom-left-radius: $border-radius-default;
- border-top-left-radius: $border-radius-default;
+ border-bottom-right-radius: 0;
+ border-top-right-radius: 0;
+ line-height: $gl-line-height;
}
:not(:first-child) {
diff --git a/app/assets/stylesheets/pages/pipeline_schedules.scss b/app/assets/stylesheets/pages/pipeline_schedules.scss
index 86e70955389..617b3db2fae 100644
--- a/app/assets/stylesheets/pages/pipeline_schedules.scss
+++ b/app/assets/stylesheets/pages/pipeline_schedules.scss
@@ -39,10 +39,6 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
-
- svg {
- vertical-align: middle;
- }
}
.next-run-cell {
@@ -52,6 +48,10 @@
a {
color: $text-color;
}
+
+ svg {
+ vertical-align: middle;
+ }
}
.pipeline-schedules-user-callout {
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index f084adaf5d3..1d691d1d8b8 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -240,6 +240,12 @@
left: 0;
}
+ .activities-block {
+ .event-item {
+ padding-left: 40px;
+ }
+ }
+
@include media-breakpoint-down(xs) {
.cover-block {
padding-top: 20px;
@@ -267,6 +273,12 @@
margin-right: 0;
}
}
+
+ .activities-block {
+ .event-item {
+ padding-left: 0;
+ }
+ }
}
}
diff --git a/app/controllers/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb
index fdd3b4126ff..e3226c86b0b 100644
--- a/app/controllers/admin/appearances_controller.rb
+++ b/app/controllers/admin/appearances_controller.rb
@@ -33,21 +33,21 @@ class Admin::AppearancesController < Admin::ApplicationController
@appearance.save
- redirect_to admin_appearances_path, notice: 'Logo was succesfully removed.'
+ redirect_to admin_appearances_path, notice: 'Logo was successfully removed.'
end
def header_logos
@appearance.remove_header_logo!
@appearance.save
- redirect_to admin_appearances_path, notice: 'Header logo was succesfully removed.'
+ redirect_to admin_appearances_path, notice: 'Header logo was successfully removed.'
end
def favicon
@appearance.remove_favicon!
@appearance.save
- redirect_to admin_appearances_path, notice: 'Favicon was succesfully removed.'
+ redirect_to admin_appearances_path, notice: 'Favicon was successfully removed.'
end
private
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 8040a14ef56..8f683ca06ad 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -61,7 +61,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
format.html do
usage_data_json = JSON.pretty_generate(Gitlab::UsageData.data)
- render html: Gitlab::Highlight.highlight('payload.json', usage_data_json)
+ render html: Gitlab::Highlight.highlight('payload.json', usage_data_json, language: 'json')
end
format.json { render json: Gitlab::UsageData.to_json }
end
diff --git a/app/controllers/chaos_controller.rb b/app/controllers/chaos_controller.rb
new file mode 100644
index 00000000000..b4f46cddbe9
--- /dev/null
+++ b/app/controllers/chaos_controller.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+class ChaosController < ActionController::Base
+ before_action :validate_request
+
+ def leakmem
+ memory_mb = (params[:memory_mb]&.to_i || 100)
+ duration_s = (params[:duration_s]&.to_i || 30).seconds
+
+ start = Time.now
+ retainer = []
+ # Add `n` 1mb chunks of memory to the retainer array
+ memory_mb.times { retainer << "x" * 1.megabyte }
+
+ duration_taken = (Time.now - start).seconds
+ Kernel.sleep duration_s - duration_taken if duration_s > duration_taken
+
+ render text: "OK", content_type: 'text/plain'
+ end
+
+ def cpuspin
+ duration_s = (params[:duration_s]&.to_i || 30).seconds
+ end_time = Time.now + duration_s.seconds
+
+ rand while Time.now < end_time
+
+ render text: "OK", content_type: 'text/plain'
+ end
+
+ def sleep
+ duration_s = (params[:duration_s]&.to_i || 30).seconds
+ Kernel.sleep duration_s
+
+ render text: "OK", content_type: 'text/plain'
+ end
+
+ def kill
+ Process.kill("KILL", Process.pid)
+ end
+
+ private
+
+ def validate_request
+ secret = ENV['GITLAB_CHAOS_SECRET']
+ # GITLAB_CHAOS_SECRET is required unless you're running in Development mode
+ if !secret && !Rails.env.development?
+ render text: "chaos misconfigured: please configure GITLAB_CHAOS_SECRET when using GITLAB_ENABLE_CHAOS_ENDPOINTS outside of a development environment", content_type: 'text/plain', status: 500
+ end
+
+ return unless secret
+
+ unless request.headers["HTTP_X_CHAOS_SECRET"] == secret
+ render text: "To experience chaos, please set X-Chaos-Secret header", content_type: 'text/plain', status: 401
+ end
+ end
+end
diff --git a/app/controllers/clusters/applications_controller.rb b/app/controllers/clusters/applications_controller.rb
new file mode 100644
index 00000000000..250f42f3096
--- /dev/null
+++ b/app/controllers/clusters/applications_controller.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class Clusters::ApplicationsController < Clusters::BaseController
+ before_action :cluster
+ before_action :authorize_create_cluster!, only: [:create]
+
+ def create
+ Clusters::Applications::CreateService
+ .new(@cluster, current_user, create_cluster_application_params)
+ .execute(request)
+
+ head :no_content
+ rescue Clusters::Applications::CreateService::InvalidApplicationError
+ render_404
+ rescue StandardError
+ head :bad_request
+ end
+
+ private
+
+ def cluster
+ @cluster ||= clusterable.clusters.find(params[:id]) || render_404
+ end
+
+ def create_cluster_application_params
+ params.permit(:application, :hostname)
+ end
+end
diff --git a/app/controllers/clusters/base_controller.rb b/app/controllers/clusters/base_controller.rb
new file mode 100644
index 00000000000..ef42f7c4074
--- /dev/null
+++ b/app/controllers/clusters/base_controller.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+class Clusters::BaseController < ApplicationController
+ include RoutableActions
+
+ skip_before_action :authenticate_user!
+ before_action :authorize_read_cluster!
+
+ helper_method :clusterable
+
+ private
+
+ def cluster
+ @cluster ||= clusterable.clusters.find(params[:id])
+ .present(current_user: current_user)
+ end
+
+ def authorize_update_cluster!
+ access_denied! unless can?(current_user, :update_cluster, cluster)
+ end
+
+ def authorize_admin_cluster!
+ access_denied! unless can?(current_user, :admin_cluster, cluster)
+ end
+
+ def authorize_read_cluster!
+ access_denied! unless can?(current_user, :read_cluster, clusterable)
+ end
+
+ def authorize_create_cluster!
+ access_denied! unless can?(current_user, :create_cluster, clusterable)
+ end
+
+ def clusterable
+ raise NotImplementedError
+ end
+end
diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb
new file mode 100644
index 00000000000..2e9c77ae55c
--- /dev/null
+++ b/app/controllers/clusters/clusters_controller.rb
@@ -0,0 +1,218 @@
+# frozen_string_literal: true
+
+class Clusters::ClustersController < Clusters::BaseController
+ include RoutableActions
+
+ before_action :cluster, except: [:index, :new, :create_gcp, :create_user]
+ before_action :generate_gcp_authorize_url, only: [:new]
+ before_action :validate_gcp_token, only: [:new]
+ before_action :gcp_cluster, only: [:new]
+ before_action :user_cluster, only: [:new]
+ before_action :authorize_create_cluster!, only: [:new]
+ before_action :authorize_update_cluster!, only: [:update]
+ before_action :authorize_admin_cluster!, only: [:destroy]
+ before_action :update_applications_status, only: [:cluster_status]
+
+ helper_method :token_in_session
+
+ STATUS_POLLING_INTERVAL = 10_000
+
+ def index
+ clusters = ClustersFinder.new(clusterable, current_user, :all).execute
+ @clusters = clusters.page(params[:page]).per(20)
+ end
+
+ def new
+ end
+
+ # Overridding ActionController::Metal#status is NOT a good idea
+ def cluster_status
+ respond_to do |format|
+ format.json do
+ Gitlab::PollingInterval.set_header(response, interval: STATUS_POLLING_INTERVAL)
+
+ render json: ClusterSerializer
+ .new(current_user: @current_user)
+ .represent_status(@cluster)
+ end
+ end
+ end
+
+ def show
+ end
+
+ def update
+ Clusters::UpdateService
+ .new(current_user, update_params)
+ .execute(cluster)
+
+ if cluster.valid?
+ respond_to do |format|
+ format.json do
+ head :no_content
+ end
+ format.html do
+ flash[:notice] = _('Kubernetes cluster was successfully updated.')
+ redirect_to cluster.show_path
+ end
+ end
+ else
+ respond_to do |format|
+ format.json { head :bad_request }
+ format.html { render :show }
+ end
+ end
+ end
+
+ def destroy
+ if cluster.destroy
+ flash[:notice] = _('Kubernetes cluster integration was successfully removed.')
+ redirect_to clusterable.index_path, status: :found
+ else
+ flash[:notice] = _('Kubernetes cluster integration was not removed.')
+ render :show
+ end
+ end
+
+ def create_gcp
+ @gcp_cluster = ::Clusters::CreateService
+ .new(current_user, create_gcp_cluster_params)
+ .execute(access_token: token_in_session)
+ .present(current_user: current_user)
+
+ if @gcp_cluster.persisted?
+ redirect_to @gcp_cluster.show_path
+ else
+ generate_gcp_authorize_url
+ validate_gcp_token
+ user_cluster
+
+ render :new, locals: { active_tab: 'gcp' }
+ end
+ end
+
+ def create_user
+ @user_cluster = ::Clusters::CreateService
+ .new(current_user, create_user_cluster_params)
+ .execute(access_token: token_in_session)
+ .present(current_user: current_user)
+
+ if @user_cluster.persisted?
+ redirect_to @user_cluster.show_path
+ else
+ generate_gcp_authorize_url
+ validate_gcp_token
+ gcp_cluster
+
+ render :new, locals: { active_tab: 'user' }
+ end
+ end
+
+ private
+
+ def update_params
+ if cluster.managed?
+ params.require(:cluster).permit(
+ :enabled,
+ :environment_scope,
+ platform_kubernetes_attributes: [
+ :namespace
+ ]
+ )
+ else
+ params.require(:cluster).permit(
+ :enabled,
+ :name,
+ :environment_scope,
+ platform_kubernetes_attributes: [
+ :api_url,
+ :token,
+ :ca_cert,
+ :namespace
+ ]
+ )
+ end
+ end
+
+ def create_gcp_cluster_params
+ params.require(:cluster).permit(
+ :enabled,
+ :name,
+ :environment_scope,
+ provider_gcp_attributes: [
+ :gcp_project_id,
+ :zone,
+ :num_nodes,
+ :machine_type,
+ :legacy_abac
+ ]).merge(
+ provider_type: :gcp,
+ platform_type: :kubernetes,
+ clusterable: clusterable.subject
+ )
+ end
+
+ def create_user_cluster_params
+ params.require(:cluster).permit(
+ :enabled,
+ :name,
+ :environment_scope,
+ platform_kubernetes_attributes: [
+ :namespace,
+ :api_url,
+ :token,
+ :ca_cert,
+ :authorization_type
+ ]).merge(
+ provider_type: :user,
+ platform_type: :kubernetes,
+ clusterable: clusterable.subject
+ )
+ end
+
+ def generate_gcp_authorize_url
+ state = generate_session_key_redirect(clusterable.new_path.to_s)
+
+ @authorize_url = GoogleApi::CloudPlatform::Client.new(
+ nil, callback_google_api_auth_url,
+ state: state).authorize_url
+ rescue GoogleApi::Auth::ConfigMissingError
+ # no-op
+ end
+
+ def gcp_cluster
+ @gcp_cluster = ::Clusters::Cluster.new.tap do |cluster|
+ cluster.build_provider_gcp
+ end.present(current_user: current_user)
+ end
+
+ def user_cluster
+ @user_cluster = ::Clusters::Cluster.new.tap do |cluster|
+ cluster.build_platform_kubernetes
+ end.present(current_user: current_user)
+ end
+
+ def validate_gcp_token
+ @valid_gcp_token = GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
+ .validate_token(expires_at_in_session)
+ end
+
+ def token_in_session
+ session[GoogleApi::CloudPlatform::Client.session_key_for_token]
+ end
+
+ def expires_at_in_session
+ @expires_at_in_session ||=
+ session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]
+ end
+
+ def generate_session_key_redirect(uri)
+ GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key|
+ session[key] = uri
+ end
+ end
+
+ def update_applications_status
+ @cluster.applications.each(&:schedule_status_update)
+ end
+end
diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb
index f644702cbdb..b3777fd2b0f 100644
--- a/app/controllers/concerns/creates_commit.rb
+++ b/app/controllers/concerns/creates_commit.rb
@@ -86,10 +86,10 @@ module CreatesCommit
def new_merge_request_path
project_new_merge_request_path(
@project_to_commit_into,
- merge_request_source_branch: @branch_name,
merge_request: {
source_project_id: @project_to_commit_into.id,
target_project_id: @project.id,
+ source_branch: @branch_name,
target_branch: @start_branch
}
)
diff --git a/app/controllers/concerns/members_presentation.rb b/app/controllers/concerns/members_presentation.rb
index c6c3598a976..0a9d3d86245 100644
--- a/app/controllers/concerns/members_presentation.rb
+++ b/app/controllers/concerns/members_presentation.rb
@@ -12,12 +12,7 @@ module MembersPresentation
).fabricate!
end
- # rubocop: disable CodeReuse/ActiveRecord
def preload_associations(members)
- ActiveRecord::Associations::Preloader.new.preload(members, :user)
- ActiveRecord::Associations::Preloader.new.preload(members, :source)
- ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :status)
- ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :u2f_registrations)
+ MembersPreloader.new(members).preload_all
end
- # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/controllers/concerns/project_unauthorized.rb b/app/controllers/concerns/project_unauthorized.rb
new file mode 100644
index 00000000000..f59440dbc59
--- /dev/null
+++ b/app/controllers/concerns/project_unauthorized.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module ProjectUnauthorized
+ extend ActiveSupport::Concern
+
+ # EE would override this
+ def project_unauthorized_proc
+ # no-op
+ end
+end
diff --git a/app/controllers/concerns/routable_actions.rb b/app/controllers/concerns/routable_actions.rb
index 88939b002b2..5624eb3aa45 100644
--- a/app/controllers/concerns/routable_actions.rb
+++ b/app/controllers/concerns/routable_actions.rb
@@ -3,23 +3,25 @@
module RoutableActions
extend ActiveSupport::Concern
- def find_routable!(routable_klass, requested_full_path, extra_authorization_proc: nil)
+ def find_routable!(routable_klass, requested_full_path, extra_authorization_proc: nil, not_found_or_authorized_proc: nil)
routable = routable_klass.find_by_full_path(requested_full_path, follow_redirects: request.get?)
if routable_authorized?(routable, extra_authorization_proc)
ensure_canonical_path(routable, requested_full_path)
routable
else
- handle_not_found_or_authorized(routable)
+ if not_found_or_authorized_proc
+ not_found_or_authorized_proc.call(routable)
+ end
+
+ route_not_found unless performed?
+
nil
end
end
- # This is overridden in gitlab-ee.
- def handle_not_found_or_authorized(_routable)
- route_not_found
- end
-
def routable_authorized?(routable, extra_authorization_proc)
+ return false unless routable
+
action = :"read_#{routable.class.to_s.underscore}"
return false unless can?(current_user, action, routable)
diff --git a/app/controllers/concerns/send_file_upload.rb b/app/controllers/concerns/send_file_upload.rb
index 0bb7b7efed0..515a9eede8e 100644
--- a/app/controllers/concerns/send_file_upload.rb
+++ b/app/controllers/concerns/send_file_upload.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module SendFileUpload
- def send_upload(file_upload, send_params: {}, redirect_params: {}, attachment: nil, disposition: 'attachment')
+ def send_upload(file_upload, send_params: {}, redirect_params: {}, attachment: nil, proxy: false, disposition: 'attachment')
if attachment
# Response-Content-Type will not override an existing Content-Type in
# Google Cloud Storage, so the metadata needs to be cleared on GCS for
@@ -17,7 +17,7 @@ module SendFileUpload
if file_upload.file_storage?
send_file file_upload.path, send_params
- elsif file_upload.class.proxy_download_enabled?
+ elsif file_upload.class.proxy_download_enabled? || proxy
headers.store(*Gitlab::Workhorse.send_url(file_upload.url(**redirect_params)))
head :ok
else
diff --git a/app/controllers/groups/clusters/applications_controller.rb b/app/controllers/groups/clusters/applications_controller.rb
new file mode 100644
index 00000000000..8dd8a01cf40
--- /dev/null
+++ b/app/controllers/groups/clusters/applications_controller.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class Groups::Clusters::ApplicationsController < Clusters::ApplicationsController
+ include ControllerWithCrossProjectAccessCheck
+
+ prepend_before_action :group
+ requires_cross_project_access
+
+ private
+
+ def clusterable
+ @clusterable ||= ClusterablePresenter.fabricate(group, current_user: current_user)
+ end
+
+ def group
+ @group ||= find_routable!(Group, params[:group_id] || params[:id])
+ end
+end
diff --git a/app/controllers/groups/clusters_controller.rb b/app/controllers/groups/clusters_controller.rb
new file mode 100644
index 00000000000..50c44b7a58b
--- /dev/null
+++ b/app/controllers/groups/clusters_controller.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class Groups::ClustersController < Clusters::ClustersController
+ include ControllerWithCrossProjectAccessCheck
+
+ prepend_before_action :check_group_clusters_feature_flag!
+ prepend_before_action :group
+ requires_cross_project_access
+
+ layout 'group'
+
+ private
+
+ def clusterable
+ @clusterable ||= ClusterablePresenter.fabricate(group, current_user: current_user)
+ end
+
+ def group
+ @group ||= find_routable!(Group, params[:group_id] || params[:id])
+ end
+
+ def check_group_clusters_feature_flag!
+ render_404 unless Feature.enabled?(:group_clusters)
+ end
+end
diff --git a/app/controllers/import/bitbucket_server_controller.rb b/app/controllers/import/bitbucket_server_controller.rb
index fdd1078cdf7..575c40d5f6f 100644
--- a/app/controllers/import/bitbucket_server_controller.rb
+++ b/app/controllers/import/bitbucket_server_controller.rb
@@ -54,14 +54,14 @@ class Import::BitbucketServerController < Import::BaseController
# rubocop: disable CodeReuse/ActiveRecord
def status
- repos = bitbucket_client.repos
+ @collection = bitbucket_client.repos(page_offset: page_offset, limit: limit_per_page)
+ @repos, @incompatible_repos = @collection.partition { |repo| repo.valid? }
- @repos, @incompatible_repos = repos.partition { |repo| repo.valid? }
-
- @already_added_projects = find_already_added_projects('bitbucket_server')
+ # Use the import URL to filter beyond what BaseService#find_already_added_projects
+ @already_added_projects = filter_added_projects('bitbucket_server', @repos.map(&:browse_url))
already_added_projects_names = @already_added_projects.pluck(:import_source)
- @repos.to_a.reject! { |repo| already_added_projects_names.include?(repo.browse_url) }
+ @repos.reject! { |repo| already_added_projects_names.include?(repo.browse_url) }
rescue BitbucketServer::Connection::ConnectionError, BitbucketServer::Client::ServerError => e
flash[:alert] = "Unable to connect to server: #{e}"
clear_session_data
@@ -75,6 +75,12 @@ class Import::BitbucketServerController < Import::BaseController
private
+ # rubocop: disable CodeReuse/ActiveRecord
+ def filter_added_projects(import_type, import_sources)
+ current_user.created_projects.where(import_type: import_type, import_source: import_sources).includes(:import_state)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
def bitbucket_client
@bitbucket_client ||= BitbucketServer::Client.new(credentials)
end
@@ -130,4 +136,12 @@ class Import::BitbucketServerController < Import::BaseController
password: session[personal_access_token_key]
}
end
+
+ def page_offset
+ [0, params[:page].to_i].max
+ end
+
+ def limit_per_page
+ BitbucketServer::Paginator::PAGE_LENGTH
+ end
end
diff --git a/app/controllers/import/gitea_controller.rb b/app/controllers/import/gitea_controller.rb
index 382c684a408..f067ef625aa 100644
--- a/app/controllers/import/gitea_controller.rb
+++ b/app/controllers/import/gitea_controller.rb
@@ -23,7 +23,7 @@ class Import::GiteaController < Import::GithubController
:"#{provider}_host_url"
end
- # Overriden methods
+ # Overridden methods
def provider
:gitea
end
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index e3eec5a020d..58565aaf8c9 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -103,7 +103,7 @@ class Import::GithubController < Import::BaseController
{ github_access_token: session[access_token_key] }
end
- # The following methods are overriden in subclasses
+ # The following methods are overridden in subclasses
def provider
:github
end
diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb
index 894a6a431e3..705389749d8 100644
--- a/app/controllers/oauth/authorizations_controller.rb
+++ b/app/controllers/oauth/authorizations_controller.rb
@@ -3,7 +3,7 @@
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
layout 'profile'
- # Overriden from Doorkeeper::AuthorizationsController to
+ # Overridden from Doorkeeper::AuthorizationsController to
# include the call to session.delete
def new
if pre_auth.authorizable?
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index a2bdcaefa9b..e0677ce3fbc 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -3,6 +3,7 @@
class Projects::ApplicationController < ApplicationController
include CookiesHelper
include RoutableActions
+ include ProjectUnauthorized
include ChecksCollaboration
skip_before_action :authenticate_user!
@@ -21,7 +22,7 @@ class Projects::ApplicationController < ApplicationController
path = File.join(params[:namespace_id], params[:project_id] || params[:id])
auth_proc = ->(project) { !project.pending_delete? }
- @project = find_routable!(Project, path, extra_authorization_proc: auth_proc)
+ @project = find_routable!(Project, path, extra_authorization_proc: auth_proc, not_found_or_authorized_proc: project_unauthorized_proc)
end
def build_canonical_path(project)
diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
index 312e256ea6c..ae9c17802b9 100644
--- a/app/controllers/projects/artifacts_controller.rb
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -16,7 +16,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
def download
return render_404 unless artifacts_file
- send_upload(artifacts_file, attachment: artifacts_file.filename)
+ send_upload(artifacts_file, attachment: artifacts_file.filename, proxy: params[:proxy])
end
def browse
diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb
index d386fb63d9f..9c130af8394 100644
--- a/app/controllers/projects/autocomplete_sources_controller.rb
+++ b/app/controllers/projects/autocomplete_sources_controller.rb
@@ -1,40 +1,38 @@
# frozen_string_literal: true
class Projects::AutocompleteSourcesController < Projects::ApplicationController
- before_action :load_autocomplete_service, except: [:members]
-
def members
render json: ::Projects::ParticipantsService.new(@project, current_user).execute(target)
end
def issues
- render json: @autocomplete_service.issues
+ render json: autocomplete_service.issues
end
def merge_requests
- render json: @autocomplete_service.merge_requests
+ render json: autocomplete_service.merge_requests
end
def labels
- render json: @autocomplete_service.labels_as_hash(target)
+ render json: autocomplete_service.labels_as_hash(target)
end
def milestones
- render json: @autocomplete_service.milestones
+ render json: autocomplete_service.milestones
end
def commands
- render json: @autocomplete_service.commands(target, params[:type])
+ render json: autocomplete_service.commands(target, params[:type])
end
def snippets
- render json: @autocomplete_service.snippets
+ render json: autocomplete_service.snippets
end
private
- def load_autocomplete_service
- @autocomplete_service = ::Projects::AutocompleteService.new(@project, current_user)
+ def autocomplete_service
+ @autocomplete_service ||= ::Projects::AutocompleteService.new(@project, current_user)
end
def target
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 56a884b8a2a..2a6fe3b9c97 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -92,7 +92,7 @@ class Projects::BlobController < Projects::ApplicationController
apply_diff_view_cookie!
@blob.load_all_data!
- @lines = Gitlab::Highlight.highlight(@blob.path, @blob.data, repository: @repository).lines
+ @lines = @blob.present.highlight.lines
@form = UnfoldForm.new(params)
@@ -122,7 +122,7 @@ class Projects::BlobController < Projects::ApplicationController
@lines.map! do |line|
# These are marked as context lines but are loaded from blobs.
# We also have context lines loaded from diffs in other places.
- diff_line = Gitlab::Diff::Line.new(line, 'context', nil, nil, nil)
+ diff_line = Gitlab::Diff::Line.new(line, nil, nil, nil, nil)
diff_line.rich_text = line
diff_line
end
diff --git a/app/controllers/projects/clusters/applications_controller.rb b/app/controllers/projects/clusters/applications_controller.rb
index bcea96bce94..c7b6218d007 100644
--- a/app/controllers/projects/clusters/applications_controller.rb
+++ b/app/controllers/projects/clusters/applications_controller.rb
@@ -1,29 +1,17 @@
# frozen_string_literal: true
-class Projects::Clusters::ApplicationsController < Projects::ApplicationController
- before_action :cluster
- before_action :authorize_read_cluster!
- before_action :authorize_create_cluster!, only: [:create]
+class Projects::Clusters::ApplicationsController < Clusters::ApplicationsController
+ include ProjectUnauthorized
- def create
- Clusters::Applications::CreateService
- .new(@cluster, current_user, create_cluster_application_params)
- .execute(request)
-
- head :no_content
- rescue Clusters::Applications::CreateService::InvalidApplicationError
- render_404
- rescue StandardError
- head :bad_request
- end
+ prepend_before_action :project
private
- def cluster
- @cluster ||= project.clusters.find(params[:id]) || render_404
+ def clusterable
+ @clusterable ||= ClusterablePresenter.fabricate(project, current_user: current_user)
end
- def create_cluster_application_params
- params.permit(:application, :hostname)
+ def project
+ @project ||= find_routable!(Project, File.join(params[:namespace_id], params[:project_id]), not_found_or_authorized_proc: project_unauthorized_proc)
end
end
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb
index 62adc66fb09..feda6deeaa6 100644
--- a/app/controllers/projects/clusters_controller.rb
+++ b/app/controllers/projects/clusters_controller.rb
@@ -1,224 +1,24 @@
# frozen_string_literal: true
-class Projects::ClustersController < Projects::ApplicationController
- before_action :cluster, except: [:index, :new, :create_gcp, :create_user]
- before_action :authorize_read_cluster!
- before_action :generate_gcp_authorize_url, only: [:new]
- before_action :validate_gcp_token, only: [:new]
- before_action :gcp_cluster, only: [:new]
- before_action :user_cluster, only: [:new]
- before_action :authorize_create_cluster!, only: [:new]
- before_action :authorize_update_cluster!, only: [:update]
- before_action :authorize_admin_cluster!, only: [:destroy]
- before_action :update_applications_status, only: [:status]
- helper_method :token_in_session
+class Projects::ClustersController < Clusters::ClustersController
+ include ProjectUnauthorized
- STATUS_POLLING_INTERVAL = 10_000
+ prepend_before_action :project
+ before_action :repository
- def index
- clusters = ClustersFinder.new(project, current_user, :all).execute
- @clusters = clusters.page(params[:page]).per(20)
- end
-
- def new
- end
-
- def status
- respond_to do |format|
- format.json do
- Gitlab::PollingInterval.set_header(response, interval: STATUS_POLLING_INTERVAL)
-
- render json: ClusterSerializer
- .new(project: @project, current_user: @current_user)
- .represent_status(@cluster)
- end
- end
- end
-
- def show
- end
-
- def update
- Clusters::UpdateService
- .new(current_user, update_params)
- .execute(cluster)
-
- if cluster.valid?
- respond_to do |format|
- format.json do
- head :no_content
- end
- format.html do
- flash[:notice] = _('Kubernetes cluster was successfully updated.')
- redirect_to project_cluster_path(project, cluster)
- end
- end
- else
- respond_to do |format|
- format.json { head :bad_request }
- format.html { render :show }
- end
- end
- end
-
- def destroy
- if cluster.destroy
- flash[:notice] = _('Kubernetes cluster integration was successfully removed.')
- redirect_to project_clusters_path(project), status: :found
- else
- flash[:notice] = _('Kubernetes cluster integration was not removed.')
- render :show
- end
- end
-
- def create_gcp
- @gcp_cluster = ::Clusters::CreateService
- .new(current_user, create_gcp_cluster_params)
- .execute(project: project, access_token: token_in_session)
-
- if @gcp_cluster.persisted?
- redirect_to project_cluster_path(project, @gcp_cluster)
- else
- generate_gcp_authorize_url
- validate_gcp_token
- user_cluster
-
- render :new, locals: { active_tab: 'gcp' }
- end
- end
-
- def create_user
- @user_cluster = ::Clusters::CreateService
- .new(current_user, create_user_cluster_params)
- .execute(project: project, access_token: token_in_session)
-
- if @user_cluster.persisted?
- redirect_to project_cluster_path(project, @user_cluster)
- else
- generate_gcp_authorize_url
- validate_gcp_token
- gcp_cluster
-
- render :new, locals: { active_tab: 'user' }
- end
- end
+ layout 'project'
private
- def cluster
- @cluster ||= project.clusters.find(params[:id])
- .present(current_user: current_user)
- end
-
- def update_params
- if cluster.managed?
- params.require(:cluster).permit(
- :enabled,
- :environment_scope,
- platform_kubernetes_attributes: [
- :namespace
- ]
- )
- else
- params.require(:cluster).permit(
- :enabled,
- :name,
- :environment_scope,
- platform_kubernetes_attributes: [
- :api_url,
- :token,
- :ca_cert,
- :namespace
- ]
- )
- end
- end
-
- def create_gcp_cluster_params
- params.require(:cluster).permit(
- :enabled,
- :name,
- :environment_scope,
- provider_gcp_attributes: [
- :gcp_project_id,
- :zone,
- :num_nodes,
- :machine_type,
- :legacy_abac
- ]).merge(
- provider_type: :gcp,
- platform_type: :kubernetes
- )
- end
-
- def create_user_cluster_params
- params.require(:cluster).permit(
- :enabled,
- :name,
- :environment_scope,
- platform_kubernetes_attributes: [
- :namespace,
- :api_url,
- :token,
- :ca_cert,
- :authorization_type
- ]).merge(
- provider_type: :user,
- platform_type: :kubernetes
- )
- end
-
- def generate_gcp_authorize_url
- state = generate_session_key_redirect(new_project_cluster_path(@project).to_s)
-
- @authorize_url = GoogleApi::CloudPlatform::Client.new(
- nil, callback_google_api_auth_url,
- state: state).authorize_url
- rescue GoogleApi::Auth::ConfigMissingError
- # no-op
- end
-
- def gcp_cluster
- @gcp_cluster = ::Clusters::Cluster.new.tap do |cluster|
- cluster.build_provider_gcp
- end
- end
-
- def user_cluster
- @user_cluster = ::Clusters::Cluster.new.tap do |cluster|
- cluster.build_platform_kubernetes
- end
- end
-
- def validate_gcp_token
- @valid_gcp_token = GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
- .validate_token(expires_at_in_session)
- end
-
- def token_in_session
- session[GoogleApi::CloudPlatform::Client.session_key_for_token]
- end
-
- def expires_at_in_session
- @expires_at_in_session ||=
- session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]
- end
-
- def generate_session_key_redirect(uri)
- GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key|
- session[key] = uri
- end
- end
-
- def authorize_update_cluster!
- access_denied! unless can?(current_user, :update_cluster, cluster)
+ def clusterable
+ @clusterable ||= ClusterablePresenter.fabricate(project, current_user: current_user)
end
- def authorize_admin_cluster!
- access_denied! unless can?(current_user, :admin_cluster, cluster)
+ def project
+ @project ||= find_routable!(Project, File.join(params[:namespace_id], params[:project_id]), not_found_or_authorized_proc: project_unauthorized_proc)
end
- def update_applications_status
- @cluster.applications.each(&:schedule_status_update)
+ def repository
+ @repository ||= project.repository
end
end
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 00b63f55710..32fc5140366 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -43,7 +43,7 @@ class Projects::CommitController < Projects::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord
def pipelines
@pipelines = @commit.pipelines.order(id: :desc)
- @pipelines = @pipelines.where(ref: params[:ref]) if params[:ref]
+ @pipelines = @pipelines.where(ref: params[:ref]).page(params[:page]).per(30) if params[:ref]
respond_to do |format|
format.html
@@ -53,6 +53,7 @@ class Projects::CommitController < Projects::ApplicationController
render json: {
pipelines: PipelineSerializer
.new(project: @project, current_user: @current_user)
+ .with_pagination(request, response)
.represent(@pipelines),
count: {
all: @pipelines.count
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb
index bbf662a63c8..5639402a1e9 100644
--- a/app/controllers/projects/merge_requests/creations_controller.rb
+++ b/app/controllers/projects/merge_requests/creations_controller.rb
@@ -89,8 +89,6 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
def build_merge_request
params[:merge_request] ||= ActionController::Parameters.new(source_project: @project)
- params[:merge_request][:source_branch] ||= params[:merge_request_source_branch].presence
-
@merge_request = ::MergeRequests::BuildService.new(project, current_user, merge_request_params.merge(diff_options: diff_options)).execute
end
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index 5307cd0720a..b3d77335c2a 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -22,6 +22,12 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
def render_diffs
@environment = @merge_request.environments_for(current_user).last
+ notes_grouped_by_path = renderable_notes.group_by { |note| note.position.file_path }
+
+ @diffs.diff_files.each do |diff_file|
+ notes = notes_grouped_by_path.fetch(diff_file.file_path, [])
+ notes.each { |note| diff_file.unfold_diff_lines(note.position) }
+ end
@diffs.write_cache
@@ -108,4 +114,10 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
@grouped_diff_discussions = @merge_request.grouped_diff_discussions(@compare.diff_refs)
@notes = prepare_notes_for_rendering(@grouped_diff_discussions.values.flatten.flat_map(&:notes), @merge_request)
end
+
+ def renderable_notes
+ define_diff_comment_vars unless @notes
+
+ @notes
+ end
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 27b83da4f54..23d16fed7b9 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -14,9 +14,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action :set_issuables_index, only: [:index]
before_action :authenticate_user!, only: [:assign_related_issues]
before_action :check_user_can_push_to_source_branch!, only: [:rebase]
- before_action do
- push_frontend_feature_flag(:ci_environments_status_changes)
- end
def index
@merge_requests = @issuables
@@ -84,13 +81,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
def pipelines
- @pipelines = @merge_request.all_pipelines
+ @pipelines = @merge_request.all_pipelines.page(params[:page]).per(30)
Gitlab::PollingInterval.set_header(response, interval: 10_000)
render json: {
pipelines: PipelineSerializer
.new(project: @project, current_user: @current_user)
+ .with_pagination(request, response)
.represent(@pipelines),
count: {
all: @pipelines.count
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index ee438e160f2..7f4a9f5151b 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -276,7 +276,7 @@ class ProjectsController < Projects::ApplicationController
# rubocop: enable CodeReuse/ActiveRecord
# Render project landing depending of which features are available
- # So if page is not availble in the list it renders the next page
+ # So if page is not available in the list it renders the next page
#
# pages list order: repository readme, wiki home, issues list, customize workflow
def render_landing_page
diff --git a/app/finders/autocomplete/users_finder.rb b/app/finders/autocomplete/users_finder.rb
index e2283f3266e..45955783be9 100644
--- a/app/finders/autocomplete/users_finder.rb
+++ b/app/finders/autocomplete/users_finder.rb
@@ -72,7 +72,6 @@ module Autocomplete
author_id.present? && current_user
end
- # rubocop: disable CodeReuse/ActiveRecord
def find_users
if project
project.authorized_users.union_with_user(author_id)
@@ -84,6 +83,5 @@ module Autocomplete
User.none
end
end
- # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/finders/clusters_finder.rb b/app/finders/clusters_finder.rb
index b40d6c41b71..0cce493b73e 100644
--- a/app/finders/clusters_finder.rb
+++ b/app/finders/clusters_finder.rb
@@ -1,20 +1,20 @@
# frozen_string_literal: true
class ClustersFinder
- def initialize(project, user, scope)
- @project = project
+ def initialize(clusterable, user, scope)
+ @clusterable = clusterable
@user = user
@scope = scope || :active
end
def execute
- clusters = project.clusters
+ clusters = clusterable.clusters
filter_by_scope(clusters)
end
private
- attr_reader :project, :user, :scope
+ attr_reader :clusterable, :user, :scope
def filter_by_scope(clusters)
case scope.to_sym
diff --git a/app/finders/concerns/finder_with_cross_project_access.rb b/app/finders/concerns/finder_with_cross_project_access.rb
index e038636f0c4..220f62bcc7f 100644
--- a/app/finders/concerns/finder_with_cross_project_access.rb
+++ b/app/finders/concerns/finder_with_cross_project_access.rb
@@ -16,7 +16,6 @@ module FinderWithCrossProjectAccess
end
override :execute
- # rubocop: disable CodeReuse/ActiveRecord
def execute(*args)
check = Gitlab::CrossProjectAccess.find_check(self)
original = super
@@ -30,7 +29,6 @@ module FinderWithCrossProjectAccess
original
end
end
- # rubocop: enable CodeReuse/ActiveRecord
# We can skip the cross project check for finding indivitual records.
# this would be handled by the `can?(:read_*, result)` call in `FinderMethods`
diff --git a/app/finders/group_descendants_finder.rb b/app/finders/group_descendants_finder.rb
index 9d57d2d3bc9..c96979619fd 100644
--- a/app/finders/group_descendants_finder.rb
+++ b/app/finders/group_descendants_finder.rb
@@ -131,7 +131,6 @@ class GroupDescendantsFinder
.with_selects_for_list(archived: params[:archived])
end
- # rubocop: disable CodeReuse/ActiveRecord
def subgroups
return Group.none unless Group.supports_nested_groups?
@@ -145,7 +144,6 @@ class GroupDescendantsFinder
groups.with_selects_for_list(archived: params[:archived]).order_by(sort)
end
- # rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/Finder
def direct_child_projects
diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb
index a35a3ed6142..ea954f98220 100644
--- a/app/finders/groups_finder.rb
+++ b/app/finders/groups_finder.rb
@@ -40,7 +40,6 @@ class GroupsFinder < UnionFinder
attr_reader :current_user, :params
- # rubocop: disable CodeReuse/ActiveRecord
def all_groups
return [owned_groups] if params[:owned]
return [groups_with_min_access_level] if min_access_level?
@@ -52,7 +51,6 @@ class GroupsFinder < UnionFinder
groups << Group.none if groups.empty?
groups
end
- # rubocop: enable CodeReuse/ActiveRecord
def groups_for_ancestors
current_user.authorized_groups
@@ -82,11 +80,9 @@ class GroupsFinder < UnionFinder
end
# rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
def owned_groups
current_user&.owned_groups || Group.none
end
- # rubocop: enable CodeReuse/ActiveRecord
def include_public_groups?
current_user.nil? || all_available?
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index eb3d2498830..93bef592c65 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -192,11 +192,6 @@ class IssuableFinder
params[:milestone_title].present?
end
- def filter_by_no_milestone?
- milestones? && params[:milestone_title] == Milestone::None.title
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
def milestones
return @milestones if defined?(@milestones)
@@ -217,7 +212,6 @@ class IssuableFinder
Milestone.none
end
end
- # rubocop: enable CodeReuse/ActiveRecord
def labels?
params[:label_name].present?
@@ -227,7 +221,6 @@ class IssuableFinder
labels? && params[:label_name].include?(Label::None.title)
end
- # rubocop: disable CodeReuse/ActiveRecord
def labels
return @labels if defined?(@labels)
@@ -238,7 +231,6 @@ class IssuableFinder
Label.none
end
end
- # rubocop: enable CodeReuse/ActiveRecord
def assignee_id?
params[:assignee_id].present?
@@ -248,15 +240,6 @@ class IssuableFinder
params[:assignee_username].present?
end
- def filter_by_no_assignee?
- # Assignee_id takes precedence over assignee_username
- [NONE, FILTER_NONE].include?(params[:assignee_id].to_s.downcase) || params[:assignee_username].to_s == NONE
- end
-
- def filter_by_any_assignee?
- params[:assignee_id].to_s.downcase == FILTER_ANY
- end
-
# rubocop: disable CodeReuse/ActiveRecord
def assignee
return @assignee if defined?(@assignee)
@@ -422,6 +405,15 @@ class IssuableFinder
end
# rubocop: enable CodeReuse/ActiveRecord
+ def filter_by_no_assignee?
+ # Assignee_id takes precedence over assignee_username
+ [NONE, FILTER_NONE].include?(params[:assignee_id].to_s.downcase) || params[:assignee_username].to_s == NONE
+ end
+
+ def filter_by_any_assignee?
+ params[:assignee_id].to_s.downcase == FILTER_ANY
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def by_author(items)
if author
@@ -436,18 +428,6 @@ class IssuableFinder
end
# rubocop: enable CodeReuse/ActiveRecord
- def filter_by_upcoming_milestone?
- params[:milestone_title] == Milestone::Upcoming.name
- end
-
- def filter_by_any_milestone?
- params[:milestone_title] == Milestone::Any.title
- end
-
- def filter_by_started_milestone?
- params[:milestone_title] == Milestone::Started.name
- end
-
# rubocop: disable CodeReuse/ActiveRecord
def by_milestone(items)
if milestones?
@@ -469,6 +449,24 @@ class IssuableFinder
end
# rubocop: enable CodeReuse/ActiveRecord
+ def filter_by_no_milestone?
+ # Accepts `No Milestone` for compatibility
+ params[:milestone_title].to_s.downcase == FILTER_NONE || params[:milestone_title] == Milestone::None.title
+ end
+
+ def filter_by_any_milestone?
+ # Accepts `Any Milestone` for compatibility
+ params[:milestone_title].to_s.downcase == FILTER_ANY || params[:milestone_title] == Milestone::Any.title
+ end
+
+ def filter_by_upcoming_milestone?
+ params[:milestone_title] == Milestone::Upcoming.name
+ end
+
+ def filter_by_started_milestone?
+ params[:milestone_title] == Milestone::Started.name
+ end
+
def by_label(items)
return items unless labels?
@@ -484,12 +482,27 @@ class IssuableFinder
def by_my_reaction_emoji(items)
if params[:my_reaction_emoji].present? && current_user
- items = items.awarded(current_user, params[:my_reaction_emoji])
+ items =
+ if filter_by_no_reaction?
+ items.not_awarded(current_user)
+ elsif filter_by_any_reaction?
+ items.awarded(current_user)
+ else
+ items.awarded(current_user, params[:my_reaction_emoji])
+ end
end
items
end
+ def filter_by_no_reaction?
+ params[:my_reaction_emoji].to_s.downcase == FILTER_NONE
+ end
+
+ def filter_by_any_reaction?
+ params[:my_reaction_emoji].to_s.downcase == FILTER_ANY
+ end
+
def label_names
if labels?
params[:label_name].is_a?(String) ? params[:label_name].split(',') : params[:label_name]
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index cee57a83df4..45e494725d7 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -135,7 +135,6 @@ class IssuesFinder < IssuableFinder
current_user.blank?
end
- # rubocop: disable CodeReuse/ActiveRecord
def by_assignee(items)
if filter_by_no_assignee?
items.unassigned
@@ -149,5 +148,4 @@ class IssuesFinder < IssuableFinder
items
end
end
- # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb
index d000af21be3..e523942ea4c 100644
--- a/app/finders/labels_finder.rb
+++ b/app/finders/labels_finder.rb
@@ -12,7 +12,6 @@ class LabelsFinder < UnionFinder
@params = params
end
- # rubocop: disable CodeReuse/ActiveRecord
def execute(skip_authorization: false)
@skip_authorization = skip_authorization
items = find_union(label_ids, Label) || Label.none
@@ -21,7 +20,6 @@ class LabelsFinder < UnionFinder
items = by_search(items)
sort(items)
end
- # rubocop: enable CodeReuse/ActiveRecord
private
diff --git a/app/finders/milestones_finder.rb b/app/finders/milestones_finder.rb
index 47231ea80c7..9c477978f60 100644
--- a/app/finders/milestones_finder.rb
+++ b/app/finders/milestones_finder.rb
@@ -20,7 +20,6 @@ class MilestonesFinder
@params = params
end
- # rubocop: disable CodeReuse/ActiveRecord
def execute
return Milestone.none if project_ids.empty? && group_ids.empty?
@@ -31,7 +30,6 @@ class MilestonesFinder
order(items)
end
- # rubocop: enable CodeReuse/ActiveRecord
private
diff --git a/app/finders/personal_access_tokens_finder.rb b/app/finders/personal_access_tokens_finder.rb
index 81fd3b7a547..bd95dcd323f 100644
--- a/app/finders/personal_access_tokens_finder.rb
+++ b/app/finders/personal_access_tokens_finder.rb
@@ -3,7 +3,7 @@
class PersonalAccessTokensFinder
attr_accessor :params
- delegate :build, :find, :find_by, :find_by_token, to: :execute
+ delegate :build, :find, :find_by_id, :find_by_token, to: :execute
def initialize(params = {})
@params = params
diff --git a/app/finders/pipelines_finder.rb b/app/finders/pipelines_finder.rb
index 3d0d3219a94..35d0e1acce5 100644
--- a/app/finders/pipelines_finder.rb
+++ b/app/finders/pipelines_finder.rb
@@ -12,7 +12,6 @@ class PipelinesFinder
@params = params
end
- # rubocop: disable CodeReuse/ActiveRecord
def execute
unless Ability.allowed?(current_user, :read_pipeline, project)
return Ci::Pipeline.none
@@ -28,7 +27,6 @@ class PipelinesFinder
items = by_yaml_errors(items)
sort_items(items)
end
- # rubocop: enable CodeReuse/ActiveRecord
private
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index 6ececcd4152..93d3c991846 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -88,7 +88,6 @@ class ProjectsFinder < UnionFinder
# rubocop: enable CodeReuse/ActiveRecord
# Builds a collection for an anonymous user.
- # rubocop: disable CodeReuse/ActiveRecord
def collection_without_user
if private_only? || owned_projects? || min_access_level?
Project.none
@@ -96,7 +95,6 @@ class ProjectsFinder < UnionFinder
Project.public_to_user
end
end
- # rubocop: enable CodeReuse/ActiveRecord
def owned_projects?
params[:owned].present?
diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb
index 3528e4228b2..d3774746cb8 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -1,140 +1,149 @@
# frozen_string_literal: true
-# Snippets Finder
+# Finder for retrieving snippets that a user can see, optionally scoped to a
+# project or snippets author.
#
-# Used to filter Snippets collections by a set of params
+# Basic usage:
#
-# Arguments.
+# user = User.find(1)
#
-# current_user - The current user, nil also can be used.
-# params:
-# visibility (integer) - Individual snippet visibility: Public(20), internal(10) or private(0).
-# project (Project) - Project related.
-# author (User) - Author related.
+# SnippetsFinder.new(user).execute
#
-# params are optional
+# To limit the snippets to a specific project, supply the `project:` option:
+#
+# user = User.find(1)
+# project = Project.find(1)
+#
+# SnippetsFinder.new(user, project: project).execute
+#
+# Limiting snippets to an author can be done by supplying the `author:` option:
+#
+# user = User.find(1)
+# project = Project.find(1)
+#
+# SnippetsFinder.new(user, author: user).execute
+#
+# To filter snippets using a specific visibility level, you can provide the
+# `scope:` option:
+#
+# user = User.find(1)
+# project = Project.find(1)
+#
+# SnippetsFinder.new(user, author: user, scope: :are_public).execute
+#
+# Valid `scope:` values are:
+#
+# * `:are_private`
+# * `:are_internal`
+# * `:are_public`
+#
+# Any other value will be ignored.
class SnippetsFinder < UnionFinder
- include Gitlab::Allowable
include FinderMethods
- attr_accessor :current_user, :project, :params
+ attr_accessor :current_user, :project, :author, :scope
- def initialize(current_user, params = {})
+ def initialize(current_user = nil, params = {})
@current_user = current_user
- @params = params
@project = params[:project]
+ @author = params[:author]
+ @scope = params[:scope].to_s
+
+ if project && author
+ raise(
+ ArgumentError,
+ 'Filtering by both an author and a project is not supported, ' \
+ 'as this finder is not optimised for this use case'
+ )
+ end
end
def execute
- items = init_collection
- items = by_author(items)
- items = by_visibility(items)
+ base =
+ if project
+ snippets_for_a_single_project
+ else
+ snippets_for_multiple_projects
+ end
- items.fresh
+ base.with_optional_visibility(visibility_from_scope).fresh
end
- private
-
- def init_collection
- if project.present?
- authorized_snippets_from_project
- else
- authorized_snippets
+ # Produces a query that retrieves snippets from multiple projects.
+ #
+ # The resulting query will, depending on the user's permissions, include the
+ # following collections of snippets:
+ #
+ # 1. Snippets that don't belong to any project.
+ # 2. Snippets of projects that are visible to the current user (e.g. snippets
+ # in public projects).
+ # 3. Snippets of projects that the current user is a member of.
+ #
+ # Each collection is constructed in isolation, allowing for greater control
+ # over the resulting SQL query.
+ def snippets_for_multiple_projects
+ queries = [global_snippets]
+
+ if Ability.allowed?(current_user, :read_cross_project)
+ queries << snippets_of_visible_projects
+ queries << snippets_of_authorized_projects if current_user
end
- end
- # rubocop: disable CodeReuse/ActiveRecord
- def authorized_snippets_from_project
- if can?(current_user, :read_project_snippet, project)
- if project.team.member?(current_user)
- project.snippets
- else
- project.snippets.public_to_user(current_user)
- end
- else
- Snippet.none
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- # rubocop: disable CodeReuse/ActiveRecord
- def authorized_snippets
- # This query was intentionally converted to a raw one to get it work in Rails 5.0.
- # In Rails 5.0 and 5.1 there's a bug: https://github.com/rails/arel/issues/531
- # Please convert it back when on rails 5.2 as it works again as expected since 5.2.
- Snippet.where("#{feature_available_projects} OR #{not_project_related}")
- .public_or_visible_to_user(current_user)
+ find_union(queries, Snippet)
end
- # rubocop: enable CodeReuse/ActiveRecord
- # Returns a collection of projects that is either public or visible to the
- # logged in user.
- #
- # A caller must pass in a block to modify individual parts of
- # the query, e.g. to apply .with_feature_available_for_user on top of it.
- # This is useful for performance as we can stick those additional filters
- # at the bottom of e.g. the UNION.
- # rubocop: disable CodeReuse/ActiveRecord
- def projects_for_user
- return yield(Project.public_to_user) unless current_user
-
- # If the current_user is allowed to see all projects,
- # we can shortcut and just return.
- return yield(Project.all) if current_user.full_private_access?
-
- authorized_projects = yield(Project.where('EXISTS (?)', current_user.authorizations_for_projects))
-
- levels = Gitlab::VisibilityLevel.levels_for_user(current_user)
- visible_projects = yield(Project.where(visibility_level: levels))
-
- # We use a UNION here instead of OR clauses since this results in better
- # performance.
- Project.from_union([authorized_projects, visible_projects])
+ def snippets_for_a_single_project
+ Snippet.for_project_with_user(project, current_user)
end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def feature_available_projects
- # Don't return any project related snippets if the user cannot read cross project
- return table[:id].eq(nil).to_sql unless Ability.allowed?(current_user, :read_cross_project)
- projects = projects_for_user do |part|
- part.with_feature_available_for_user(:snippets, current_user)
- end.select(:id)
-
- # This query was intentionally converted to a raw one to get it work in Rails 5.0.
- # In Rails 5.0 and 5.1 there's a bug: https://github.com/rails/arel/issues/531
- # Please convert it back when on rails 5.2 as it works again as expected since 5.2.
- "snippets.project_id IN (#{projects.to_sql})"
+ def global_snippets
+ snippets_for_author_or_visible_to_user.only_global_snippets
end
- def not_project_related
- table[:project_id].eq(nil).to_sql
+ # Returns the snippets that the current user (logged in or not) can view.
+ def snippets_of_visible_projects
+ snippets_for_author_or_visible_to_user
+ .only_include_projects_visible_to(current_user)
+ .only_include_projects_with_snippets_enabled
end
- def table
- Snippet.arel_table
+ # Returns the snippets that the currently logged in user has access to by
+ # being a member of the project the snippets belong to.
+ #
+ # This method requires that `current_user` returns a `User` instead of `nil`,
+ # and is optimised for this specific scenario.
+ def snippets_of_authorized_projects
+ base = author ? snippets_for_author : Snippet.all
+
+ base
+ .only_include_projects_with_snippets_enabled(include_private: true)
+ .only_include_authorized_projects(current_user)
end
- # rubocop: disable CodeReuse/ActiveRecord
- def by_visibility(items)
- visibility = params[:visibility] || visibility_from_scope
-
- return items unless visibility
-
- items.where(visibility_level: visibility)
+ def snippets_for_author_or_visible_to_user
+ if author
+ snippets_for_author
+ elsif current_user
+ Snippet.visible_to_or_authored_by(current_user)
+ else
+ Snippet.public_to_user
+ end
end
- # rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
- def by_author(items)
- return items unless params[:author]
+ def snippets_for_author
+ base = author.snippets
- items.where(author_id: params[:author].id)
+ if author == current_user
+ # If the current user is also the author of all snippets, then we can
+ # include private snippets.
+ base
+ else
+ base.public_to_user(current_user)
+ end
end
- # rubocop: enable CodeReuse/ActiveRecord
def visibility_from_scope
- case params[:scope].to_s
+ case scope
when 'are_private'
Snippet::PRIVATE
when 'are_internal'
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 0c9f69b6714..086bb38ce9a 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -115,6 +115,7 @@ module ApplicationSettingsHelper
:akismet_api_key,
:akismet_enabled,
:allow_local_requests_from_hooks_and_services,
+ :archive_builds_in_human_readable,
:authorized_keys_enabled,
:auto_devops_enabled,
:auto_devops_domain,
@@ -216,7 +217,8 @@ module ApplicationSettingsHelper
:user_oauth_applications,
:version_check_enabled,
:web_ide_clientside_preview_enabled,
- :diff_max_patch_bytes
+ :diff_max_patch_bytes,
+ :commit_email_hostname
]
end
diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb
index 7fc4c1a023f..5906ddabee4 100644
--- a/app/helpers/avatars_helper.rb
+++ b/app/helpers/avatars_helper.rb
@@ -22,7 +22,7 @@ module AvatarsHelper
end
def avatar_icon_for_email(email = nil, size = nil, scale = 2, only_path: true)
- user = User.find_by_any_email(email.try(:downcase))
+ user = User.find_by_any_email(email)
if user
avatar_icon_for_user(user, size, scale, only_path: only_path)
else
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index ff7f1e3a9aa..638744a1426 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -1,9 +1,8 @@
# frozen_string_literal: true
module BlobHelper
- def highlight(blob_name, blob_content, repository: nil, plain: false)
- plain ||= blob_content.length > Blob::MAXIMUM_TEXT_HIGHLIGHT_SIZE
- highlighted = Gitlab::Highlight.highlight(blob_name, blob_content, plain: plain, repository: repository)
+ def highlight(file_name, file_content, language: nil, plain: false)
+ highlighted = Gitlab::Highlight.highlight(file_name, file_content, plain: plain, language: language)
raw %(<pre class="code highlight"><code>#{highlighted}</code></pre>)
end
diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb
index 19eb763e1de..916dcb1a308 100644
--- a/app/helpers/clusters_helper.rb
+++ b/app/helpers/clusters_helper.rb
@@ -1,7 +1,8 @@
# frozen_string_literal: true
module ClustersHelper
- def has_multiple_clusters?(project)
+ # EE overrides this
+ def has_multiple_clusters?
false
end
@@ -10,7 +11,7 @@ module ClustersHelper
return unless show_gcp_signup_offer?
content_tag :section, class: 'no-animate expanded' do
- render 'projects/clusters/gcp_signup_offer_banner'
+ render 'clusters/clusters/gcp_signup_offer_banner'
end
end
end
diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb
index 57e397f6ca0..9ece8b0bc5b 100644
--- a/app/helpers/compare_helper.rb
+++ b/app/helpers/compare_helper.rb
@@ -13,8 +13,8 @@ module CompareHelper
def create_mr_path(from = params[:from], to = params[:to], project = @project)
project_new_merge_request_path(
project,
- merge_request_source_branch: to,
merge_request: {
+ source_branch: to,
target_branch: from
}
)
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index c94946a04e7..3ce2398f1de 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -91,7 +91,14 @@ module EventsHelper
words << "##{event.target_iid}" if event.target_iid
words << "in"
elsif event.target
- words << "##{event.target_iid}:"
+ prefix =
+ if event.merge_request?
+ MergeRequest.reference_prefix
+ else
+ Issue.reference_prefix
+ end
+
+ words << "#{prefix}#{event.target_iid}:" if event.target_iid
words << event.target.title if event.target.respond_to?(:title)
words << "at"
end
@@ -163,14 +170,10 @@ module EventsHelper
def event_note_title_html(event)
if event.note_target
- text = raw("#{event.note_target_type} ") +
- if event.commit_note?
- content_tag(:span, event.note_target_reference, class: 'commit-sha')
- else
- event.note_target_reference
- end
-
- link_to(text, event_note_target_url(event), title: event.target_title, class: 'has-tooltip')
+ capture do
+ concat content_tag(:span, event.note_target_type, class: "event-target-type append-right-4")
+ concat link_to(event.note_target_reference, event_note_target_url(event), title: event.target_title, class: 'has-tooltip event-target-link append-right-4')
+ end
else
content_tag(:strong, '(deleted)')
end
@@ -183,17 +186,9 @@ module EventsHelper
"--broken encoding"
end
- def event_row_class(event)
- if event.body?
- "event-block"
- else
- "event-inline"
- end
- end
-
- def icon_for_event(note)
+ def icon_for_event(note, size: 24)
icon_name = ICON_NAMES_BY_EVENT_TYPE[note]
- sprite_icon(icon_name) if icon_name
+ sprite_icon(icon_name, size: size) if icon_name
end
def icon_for_profile_event(event)
@@ -203,8 +198,24 @@ module EventsHelper
end
else
content_tag :div, class: 'system-note-image user-avatar' do
- author_avatar(event, size: 32)
+ author_avatar(event, size: 40)
end
end
end
+
+ def inline_event_icon(event)
+ unless current_path?('users#show')
+ content_tag :span, class: "system-note-image-inline d-none d-sm-flex append-right-4 #{event.action_name.parameterize}-icon align-self-center" do
+ icon_for_event(event.action_name, size: 14)
+ end
+ end
+ end
+
+ def event_user_info(event)
+ content_tag(:div, class: "event-user-info") do
+ concat content_tag(:span, link_to_author(event), class: "author_name")
+ concat "&nbsp;".html_safe
+ concat content_tag(:span, event.author.to_reference, class: "username")
+ end
+ end
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 0c313e9e6d3..e9b9b9b7721 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -140,6 +140,10 @@ module GroupsHelper
can?(current_user, "read_group_#{resource}".to_sym, @group)
end
+ if can?(current_user, :read_cluster, @group) && Feature.enabled?(:group_clusters)
+ links << :kubernetes
+ end
+
if can?(current_user, :admin_group, @group)
links << :settings
end
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index 037004327b9..b0f63de2fb8 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -109,6 +109,8 @@ module IconsHelper
def file_type_icon_class(type, mode, name)
if type == 'folder'
icon_class = 'folder'
+ elsif type == 'archive'
+ icon_class = 'archive'
elsif mode == '120000'
icon_class = 'share'
else
@@ -153,6 +155,6 @@ module IconsHelper
private
def known_sprites
- @known_sprites ||= JSON.parse(File.read(Rails.root.join('node_modules/@gitlab-org/gitlab-svgs/dist/icons.json')))['icons']
+ @known_sprites ||= JSON.parse(File.read(Rails.root.join('node_modules/@gitlab/svgs/dist/icons.json')))['icons']
end
end
diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb
index 3d0eb3d0d51..49171df1433 100644
--- a/app/helpers/import_helper.rb
+++ b/app/helpers/import_helper.rb
@@ -83,7 +83,7 @@ module ImportHelper
private
def github_project_url(full_path)
- URI.join(github_root_url, full_path).to_s
+ Gitlab::Utils.append_path(github_root_url, full_path)
end
def github_root_url
@@ -95,6 +95,6 @@ module ImportHelper
end
def gitea_project_url(full_path)
- URI.join(@gitea_host_url, full_path).to_s
+ Gitlab::Utils.append_path(@gitea_host_url, full_path)
end
end
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index 76ed8efe2c6..39f661b5f0c 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -143,7 +143,7 @@ module LabelsHelper
def labels_filter_path(options = {})
project = @target_project || @project
- format = options.delete(:format) || :html
+ format = options.delete(:format)
if project
project_labels_path(project, format, options)
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 8f549bfce73..23d7aa427bb 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -11,10 +11,10 @@ module MergeRequestsHelper
def new_mr_from_push_event(event, target_project)
{
- merge_request_source_branch: event.branch_name,
merge_request: {
source_project_id: event.project.id,
target_project_id: target_project.id,
+ source_branch: event.branch_name,
target_branch: target_project.repository.root_ref
}
}
@@ -51,10 +51,10 @@ module MergeRequestsHelper
def mr_change_branches_path(merge_request)
project_new_merge_request_path(
@project,
- merge_request_source_branch: merge_request.source_branch,
merge_request: {
source_project_id: merge_request.source_project_id,
target_project_id: merge_request.target_project_id,
+ source_branch: merge_request.source_branch,
target_branch: merge_request.target_branch
},
change_branches: true
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 761f42f2f0f..a7fe8c3d59c 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -19,10 +19,7 @@ module NavHelper
end
def page_gutter_class
- if current_path?('merge_requests#show') ||
- current_path?('projects/merge_requests/conflicts#show') ||
- current_path?('issues#show') ||
- current_path?('milestones#show')
+ if page_has_markdown?
if cookies[:collapsed_gutter] == 'true'
%w[page-gutter right-sidebar-collapsed]
@@ -50,6 +47,17 @@ module NavHelper
class_names
end
+ def show_separator?
+ Gitlab::Sherlock.enabled? || can?(current_user, :read_instance_statistics)
+ end
+
+ def page_has_markdown?
+ current_path?('merge_requests#show') ||
+ current_path?('projects/merge_requests/conflicts#show') ||
+ current_path?('issues#show') ||
+ current_path?('milestones#show')
+ end
+
private
def get_header_links
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index b33c074d1af..5038dcf9746 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -10,7 +10,7 @@ module PageLayoutHelper
@breadcrumb_title = @page_title.last
end
- # Segments are seperated by middot
+ # Segments are separated by middot
@page_title.join(" · ")
end
diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb
index 55674e37a34..df318de740a 100644
--- a/app/helpers/profiles_helper.rb
+++ b/app/helpers/profiles_helper.rb
@@ -1,6 +1,20 @@
# frozen_string_literal: true
module ProfilesHelper
+ def commit_email_select_options(user)
+ private_email = user.private_commit_email
+ verified_emails = user.verified_emails - [private_email]
+
+ [
+ [s_("Profiles|Use a private email - %{email}").html_safe % { email: private_email }, Gitlab::PrivateCommitEmail::TOKEN],
+ *verified_emails
+ ]
+ end
+
+ def selected_commit_email(user)
+ user.read_attribute(:commit_email) || user.commit_email
+ end
+
def attribute_provider_label(attribute)
user_synced_attributes_metadata = current_user.user_synced_attributes_metadata
if user_synced_attributes_metadata&.synced?(attribute)
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index d9713f9c9b0..0a7f930110a 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -540,4 +540,13 @@ module ProjectsHelper
network
]
end
+
+ def sidebar_operations_paths
+ %w[
+ environments
+ clusters
+ user
+ gcp
+ ]
+ end
end
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index 6d2da5699fb..78a11616d4c 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -31,11 +31,21 @@ module TreeHelper
# mode - File unix mode
# name - File name
def tree_icon(type, mode, name)
- icon("#{file_type_icon_class(type, mode, name)} fw")
+ icon([file_type_icon_class(type, mode, name), 'fw'])
end
- def tree_hex_class(content)
- "file_#{hexdigest(content.name)}"
+ # Using Rails `*_path` methods can be slow, especially when generating
+ # many paths, as with a repository tree that has thousands of items.
+ def fast_project_blob_path(project, blob_path)
+ Addressable::URI.escape(
+ File.join(relative_url_root, project.path_with_namespace, 'blob', blob_path)
+ )
+ end
+
+ def fast_project_tree_path(project, tree_path)
+ Addressable::URI.escape(
+ File.join(relative_url_root, project.path_with_namespace, 'tree', tree_path)
+ )
end
# Simple shortcut to File.join
@@ -142,4 +152,8 @@ module TreeHelper
def selected_branch
@branch_name || tree_edit_branch
end
+
+ def relative_url_root
+ Gitlab.config.gitlab.relative_url_root.presence || '/'
+ end
end
diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb
index bae01d476df..4aba48061ba 100644
--- a/app/helpers/user_callouts_helper.rb
+++ b/app/helpers/user_callouts_helper.rb
@@ -3,7 +3,6 @@
module UserCalloutsHelper
GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'.freeze
GCP_SIGNUP_OFFER = 'gcp_signup_offer'.freeze
- CLUSTER_SECURITY_WARNING = 'cluster_security_warning'.freeze
def show_gke_cluster_integration_callout?(project)
can?(current_user, :create_cluster, project) &&
@@ -14,10 +13,6 @@ module UserCalloutsHelper
!user_dismissed?(GCP_SIGNUP_OFFER)
end
- def show_cluster_security_warning?
- !user_dismissed?(CLUSTER_SECURITY_WARNING)
- end
-
private
def user_dismissed?(feature_name)
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index 602e5afe26b..93b51fb1774 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -45,6 +45,20 @@ module Emails
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason))
end
+ def removed_milestone_issue_email(recipient_id, issue_id, updated_by_user_id, reason = nil)
+ setup_issue_mail(issue_id, recipient_id)
+
+ mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason))
+ end
+
+ def changed_milestone_issue_email(recipient_id, issue_id, milestone, updated_by_user_id, reason = nil)
+ setup_issue_mail(issue_id, recipient_id)
+
+ @milestone = milestone
+ @milestone_url = milestone_url(@milestone)
+ mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason))
+ end
+
def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id, reason = nil)
setup_issue_mail(issue_id, recipient_id)
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index be085496731..6524d0c2087 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -40,6 +40,20 @@ module Emails
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
end
+ def removed_milestone_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason = nil)
+ setup_merge_request_mail(merge_request_id, recipient_id)
+
+ mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
+ end
+
+ def changed_milestone_merge_request_email(recipient_id, merge_request_id, milestone, updated_by_user_id, reason = nil)
+ setup_merge_request_mail(merge_request_id, recipient_id)
+
+ @milestone = milestone
+ @milestone_url = milestone_url(@milestone)
+ mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
+ end
+
def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id)
diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb
index 2f5b5483e9d..e7e8d96eca4 100644
--- a/app/mailers/previews/notify_preview.rb
+++ b/app/mailers/previews/notify_preview.rb
@@ -68,6 +68,14 @@ class NotifyPreview < ActionMailer::Preview
Notify.issue_status_changed_email(user.id, issue.id, 'closed', user.id).message
end
+ def removed_milestone_issue_email
+ Notify.removed_milestone_issue_email(user.id, issue.id, user.id)
+ end
+
+ def changed_milestone_issue_email
+ Notify.changed_milestone_issue_email(user.id, issue.id, milestone, user.id)
+ end
+
def closed_merge_request_email
Notify.closed_merge_request_email(user.id, issue.id, user.id).message
end
@@ -80,6 +88,14 @@ class NotifyPreview < ActionMailer::Preview
Notify.merged_merge_request_email(user.id, merge_request.id, user.id).message
end
+ def removed_milestone_merge_request_email
+ Notify.removed_milestone_merge_request_email(user.id, merge_request.id, user.id)
+ end
+
+ def changed_milestone_merge_request_email
+ Notify.changed_milestone_merge_request_email(user.id, merge_request.id, milestone, user.id)
+ end
+
def member_access_denied_email
Notify.member_access_denied_email('project', project.id, user.id).message
end
@@ -143,6 +159,10 @@ class NotifyPreview < ActionMailer::Preview
@merge_request ||= project.merge_requests.first
end
+ def milestone
+ @milestone ||= issue.milestone
+ end
+
def pipeline
@pipeline = Ci::Pipeline.last
end
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
new file mode 100644
index 00000000000..71fbba5b328
--- /dev/null
+++ b/app/models/application_record.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ApplicationRecord < ActiveRecord::Base
+ self.abstract_class = true
+end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index b66ec0ffab6..207ffae873a 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -5,6 +5,7 @@ class ApplicationSetting < ActiveRecord::Base
include CacheMarkdownField
include TokenAuthenticatable
include IgnorableColumn
+ include ChronicDurationAttribute
add_authentication_token_field :runners_registration_token
add_authentication_token_field :health_check_access_token
@@ -45,6 +46,8 @@ class ApplicationSetting < ActiveRecord::Base
default_value_for :id, 1
+ chronic_duration_attr_writer :archive_builds_in_human_readable, :archive_builds_in_seconds
+
validates :uuid, presence: true
validates :session_expire_delay,
@@ -184,6 +187,12 @@ class ApplicationSetting < ActiveRecord::Base
validates :user_default_internal_regex, js_regex: true, allow_nil: true
+ validates :commit_email_hostname, format: { with: /\A[^@]+\z/ }
+
+ validates :archive_builds_in_seconds,
+ allow_nil: true,
+ numericality: { only_integer: true, greater_than_or_equal_to: 1.day.seconds }
+
SUPPORTED_KEY_TYPES.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end
@@ -292,10 +301,15 @@ class ApplicationSetting < ActiveRecord::Base
user_default_internal_regex: nil,
user_show_add_ssh_key_message: true,
usage_stats_set_by_user_id: nil,
- diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES
+ diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES,
+ commit_email_hostname: default_commit_email_hostname
}
end
+ def self.default_commit_email_hostname
+ "users.noreply.#{Gitlab.config.gitlab.host}"
+ end
+
def self.create_from_defaults
create(defaults)
end
@@ -351,6 +365,10 @@ class ApplicationSetting < ActiveRecord::Base
Array(read_attribute(:repository_storages))
end
+ def commit_email_hostname
+ super.presence || self.class.default_commit_email_hostname
+ end
+
def default_project_visibility=(level)
super(Gitlab::VisibilityLevel.level_value(level))
end
@@ -441,6 +459,10 @@ class ApplicationSetting < ActiveRecord::Base
latest_terms
end
+ def archive_builds_older_than
+ archive_builds_in_seconds.seconds.ago if archive_builds_in_seconds
+ end
+
private
def ensure_uuid!
diff --git a/app/models/blob.rb b/app/models/blob.rb
index 31a839274b5..4f310e70f4f 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -1,12 +1,13 @@
# frozen_string_literal: true
-# Blob is a Rails-specific wrapper around Gitlab::Git::Blob objects
+# Blob is a Rails-specific wrapper around Gitlab::Git::Blob, SnippetBlob and Ci::ArtifactBlob
class Blob < SimpleDelegator
+ include Presentable
+ include BlobLanguageFromGitAttributes
+
CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute
CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour
- MAXIMUM_TEXT_HIGHLIGHT_SIZE = 1.megabyte
-
# Finding a viewer for a blob happens based only on extension and whether the
# blob is binary or text, which means 1 blob should only be matched by 1 viewer,
# and the order of these viewers doesn't really matter.
@@ -121,10 +122,6 @@ class Blob < SimpleDelegator
end
end
- def no_highlighting?
- raw_size && raw_size > MAXIMUM_TEXT_HIGHLIGHT_SIZE
- end
-
def empty?
raw_size == 0
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index d73c02ba5d7..889f8ce27a6 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -9,19 +9,18 @@ module Ci
include Presentable
include Importable
include Gitlab::Utils::StrongMemoize
+ include Deployable
belongs_to :project, inverse_of: :builds
belongs_to :runner
belongs_to :trigger_request
belongs_to :erased_by, class_name: 'User'
- has_many :deployments, as: :deployable
-
RUNNER_FEATURES = {
upload_multiple_artifacts: -> (build) { build.publishes_artifacts_reports? }
}.freeze
- has_one :last_deployment, -> { order('deployments.id DESC') }, as: :deployable, class_name: 'Deployment'
+ has_one :deployment, as: :deployable, class_name: 'Deployment'
has_many :trace_sections, class_name: 'Ci::BuildTraceSection'
has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id
@@ -195,6 +194,8 @@ module Ci
end
after_transition pending: :running do |build|
+ build.deployment&.run
+
build.run_after_commit do
BuildHooksWorker.perform_async(id)
end
@@ -207,6 +208,8 @@ module Ci
end
after_transition any => [:success] do |build|
+ build.deployment&.succeed
+
build.run_after_commit do
BuildSuccessWorker.perform_async(id)
PagesWorker.perform_async(:deploy, id) if build.pages_generator?
@@ -215,9 +218,10 @@ module Ci
before_transition any => [:failed] do |build|
next unless build.project
- next if build.retries_max.zero?
- if build.retries_count < build.retries_max
+ build.deployment&.drop
+
+ if build.retry_failure?
begin
Ci::Build.retry(build, build.user)
rescue Gitlab::Access::AccessDeniedError => ex
@@ -233,6 +237,10 @@ module Ci
after_transition running: any do |build|
Ci::BuildRunnerSession.where(build: build).delete_all
end
+
+ after_transition any => [:skipped, :canceled] do |build|
+ build.deployment&.cancel
+ end
end
def ensure_metadata
@@ -245,22 +253,41 @@ module Ci
.fabricate!
end
- def other_actions
+ def other_manual_actions
pipeline.manual_actions.where.not(name: name)
end
+ def other_scheduled_actions
+ pipeline.scheduled_actions.where.not(name: name)
+ end
+
def pages_generator?
Gitlab.config.pages.enabled &&
self.name == 'pages'
end
+ # degenerated build is one that cannot be run by Runner
+ def degenerated?
+ self.options.nil?
+ end
+
+ def degenerate!
+ self.update!(options: nil, yaml_variables: nil, commands: nil)
+ end
+
+ def archived?
+ return true if degenerated?
+
+ archive_builds_older_than = Gitlab::CurrentSettings.current_application_settings.archive_builds_older_than
+ archive_builds_older_than.present? && created_at < archive_builds_older_than
+ end
+
def playable?
- action? && (manual? || scheduled? || retryable?)
+ action? && !archived? && (manual? || scheduled? || retryable?)
end
def schedulable?
- Feature.enabled?('ci_enable_scheduled_build', default_enabled: true) &&
- self.when == 'delayed' && options[:start_in].present?
+ self.when == 'delayed' && options[:start_in].present?
end
def options_scheduled_at
@@ -284,7 +311,7 @@ module Ci
end
def retryable?
- success? || failed? || canceled?
+ !archived? && (success? || failed? || canceled?)
end
def retries_count
@@ -292,7 +319,17 @@ module Ci
end
def retries_max
- self.options.fetch(:retry, 0).to_i
+ normalized_retry.fetch(:max, 0)
+ end
+
+ def retry_when
+ normalized_retry.fetch(:when, ['always'])
+ end
+
+ def retry_failure?
+ return false if retries_max.zero? || retries_count >= retries_max
+
+ retry_when.include?('always') || retry_when.include?(failure_reason.to_s)
end
def latest?
@@ -323,8 +360,12 @@ module Ci
self.options.fetch(:environment, {}).fetch(:action, 'start') if self.options
end
+ def has_deployment?
+ !!self.deployment
+ end
+
def outdated_deployment?
- success? && !last_deployment.try(:last?)
+ success? && !deployment.try(:last?)
end
def depends_on_builds
@@ -339,6 +380,10 @@ module Ci
user == current_user
end
+ def on_stop
+ options&.dig(:environment, :on_stop)
+ end
+
# A slugified version of the build ref, suitable for inclusion in URLs and
# domain names. Rules:
#
@@ -706,7 +751,7 @@ module Ci
if success?
return successful_deployment_status
- elsif complete? && !success?
+ elsif failed?
return :failed
end
@@ -723,13 +768,11 @@ module Ci
end
def successful_deployment_status
- if success? && last_deployment&.last?
- return :last
- elsif success? && last_deployment.present?
- return :out_of_date
+ if deployment&.last?
+ :last
+ else
+ :out_of_date
end
-
- :creating
end
def each_report(report_types)
@@ -781,7 +824,7 @@ module Ci
end
end
- def predefined_variables
+ def predefined_variables # rubocop:disable Metrics/AbcSize
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI', value: 'true')
variables.append(key: 'GITLAB_CI', value: 'true')
@@ -801,6 +844,8 @@ module Ci
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?
+ variables.append(key: "CI_NODE_INDEX", value: self.options[:instance].to_s) if self.options&.include?(:instance)
+ variables.append(key: "CI_NODE_TOTAL", value: (self.options&.dig(:parallel) || 1).to_s)
variables.concat(legacy_variables)
end
end
@@ -849,6 +894,16 @@ module Ci
options&.dig(:environment, :url) || persisted_environment&.external_url
end
+ # The format of the retry option changed in GitLab 11.5: Before it was
+ # integer only, after it is a hash. New builds are created with the new
+ # format, but builds created before GitLab 11.5 and saved in database still
+ # have the old integer only format. This method returns the retry option
+ # normalized as a hash in 11.5+ format.
+ def normalized_retry
+ value = options&.dig(:retry)
+ value.is_a?(Integer) ? { max: value } : value.to_h
+ end
+
def build_attributes_from_config
return {} unless pipeline.config_processor
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 34a889057ab..11c88200c37 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -15,7 +15,7 @@ module Ci
metadata: nil,
trace: nil,
junit: 'junit.xml',
- codequality: 'codequality.json',
+ codequality: 'gl-code-quality-report.json',
sast: 'gl-sast-report.json',
dependency_scanning: 'gl-dependency-scanning-report.json',
container_scanning: 'gl-container-scanning-report.json',
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index aeee7f0a5d2..56010e899a4 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -181,22 +181,31 @@ module Ci
#
# ref - The name (or names) of the branch(es)/tag(s) to limit the list of
# pipelines to.
- def self.newest_first(ref = nil)
+ # limit - This limits a backlog search, default to 100.
+ def self.newest_first(ref: nil, limit: 100)
relation = order(id: :desc)
+ relation = relation.where(ref: ref) if ref
+
+ if limit
+ ids = relation.limit(limit).select(:id)
+ # MySQL does not support limit in subquery
+ ids = ids.pluck(:id) if Gitlab::Database.mysql?
+ relation = relation.where(id: ids)
+ end
- ref ? relation.where(ref: ref) : relation
+ relation
end
def self.latest_status(ref = nil)
- newest_first(ref).pluck(:status).first
+ newest_first(ref: ref).pluck(:status).first
end
def self.latest_successful_for(ref)
- newest_first(ref).success.take
+ newest_first(ref: ref).success.take
end
def self.latest_successful_for_refs(refs)
- relation = newest_first(refs).success
+ relation = newest_first(ref: refs).success
relation.each_with_object({}) do |pipeline, hash|
hash[pipeline.ref] ||= pipeline
@@ -238,6 +247,10 @@ module Ci
end
end
+ def self.latest_successful_ids_per_project
+ success.group(:project_id).select('max(id) as id')
+ end
+
def self.truncate_sha(sha)
sha[0...8]
end
diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb
new file mode 100644
index 00000000000..c66d5ce54db
--- /dev/null
+++ b/app/models/clusters/applications/knative.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Applications
+ class Knative < ActiveRecord::Base
+ VERSION = '0.1.3'.freeze
+ REPOSITORY = 'https://storage.googleapis.com/triggermesh-charts'.freeze
+
+ # This is required for helm version <= 2.10.x in order to support
+ # Setting up CRDs
+ ISTIO_CRDS = 'https://storage.googleapis.com/triggermesh-charts/istio-crds.yaml'.freeze
+
+ self.table_name = 'clusters_applications_knative'
+
+ include ::Clusters::Concerns::ApplicationCore
+ include ::Clusters::Concerns::ApplicationStatus
+ include ::Clusters::Concerns::ApplicationVersion
+ include ::Clusters::Concerns::ApplicationData
+
+ default_value_for :version, VERSION
+
+ validates :hostname, presence: true, hostname: true
+
+ def chart
+ 'knative/knative'
+ end
+
+ def values
+ { "domain" => hostname }.to_yaml
+ end
+
+ def install_command
+ Gitlab::Kubernetes::Helm::InstallCommand.new(
+ name: name,
+ version: VERSION,
+ rbac: cluster.platform_kubernetes_rbac?,
+ chart: chart,
+ files: files,
+ repository: REPOSITORY,
+ preinstall: install_script
+ )
+ end
+
+ def client
+ cluster.platform_kubernetes.kubeclient.knative_client
+ end
+
+ private
+
+ def install_script
+ ["/usr/bin/kubectl apply -f #{ISTIO_CRDS}"]
+ end
+ end
+ end
+end
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 222e4217e67..0ba056e57d4 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -3,6 +3,7 @@
module Clusters
class Cluster < ActiveRecord::Base
include Presentable
+ include Gitlab::Utils::StrongMemoize
self.table_name = 'clusters'
@@ -11,7 +12,8 @@ module Clusters
Applications::Ingress.application_name => Applications::Ingress,
Applications::Prometheus.application_name => Applications::Prometheus,
Applications::Runner.application_name => Applications::Runner,
- Applications::Jupyter.application_name => Applications::Jupyter
+ Applications::Jupyter.application_name => Applications::Jupyter,
+ Applications::Knative.application_name => Applications::Knative
}.freeze
DEFAULT_ENVIRONMENT = '*'.freeze
@@ -19,23 +21,22 @@ module Clusters
has_many :cluster_projects, class_name: 'Clusters::Project'
has_many :projects, through: :cluster_projects, class_name: '::Project'
+ has_one :cluster_project, -> { order(id: :desc) }, class_name: 'Clusters::Project'
has_many :cluster_groups, class_name: 'Clusters::Group'
has_many :groups, through: :cluster_groups, class_name: '::Group'
- has_one :cluster_group, -> { order(id: :desc) }, class_name: 'Clusters::Group'
- has_one :group, through: :cluster_group, class_name: '::Group'
-
# we force autosave to happen when we save `Cluster` model
has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true
- has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes', autosave: true
+ has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes', inverse_of: :cluster, autosave: true
has_one :application_helm, class_name: 'Clusters::Applications::Helm'
has_one :application_ingress, class_name: 'Clusters::Applications::Ingress'
has_one :application_prometheus, class_name: 'Clusters::Applications::Prometheus'
has_one :application_runner, class_name: 'Clusters::Applications::Runner'
has_one :application_jupyter, class_name: 'Clusters::Applications::Jupyter'
+ has_one :application_knative, class_name: 'Clusters::Applications::Knative'
has_many :kubernetes_namespaces
has_one :kubernetes_namespace, -> { order(id: :desc) }, class_name: 'Clusters::KubernetesNamespace'
@@ -101,7 +102,8 @@ module Clusters
application_ingress || build_application_ingress,
application_prometheus || build_application_prometheus,
application_runner || build_application_runner,
- application_jupyter || build_application_jupyter
+ application_jupyter || build_application_jupyter,
+ application_knative || build_application_knative
]
end
@@ -118,16 +120,34 @@ module Clusters
end
def first_project
- return @first_project if defined?(@first_project)
-
- @first_project = projects.first
+ strong_memoize(:first_project) do
+ projects.first
+ end
end
alias_method :project, :first_project
+ def first_group
+ strong_memoize(:first_group) do
+ groups.first
+ end
+ end
+ alias_method :group, :first_group
+
def kubeclient
platform_kubernetes.kubeclient if kubernetes?
end
+ def find_or_initialize_kubernetes_namespace(cluster_project)
+ kubernetes_namespaces.find_or_initialize_by(
+ project: cluster_project.project,
+ cluster_project: cluster_project
+ )
+ end
+
+ def allow_user_defined_namespace?
+ project_type?
+ end
+
private
def restrict_modification
diff --git a/app/models/clusters/kubernetes_namespace.rb b/app/models/clusters/kubernetes_namespace.rb
index fb5f6b65d9d..cbd52bfb48b 100644
--- a/app/models/clusters/kubernetes_namespace.rb
+++ b/app/models/clusters/kubernetes_namespace.rb
@@ -2,6 +2,8 @@
module Clusters
class KubernetesNamespace < ActiveRecord::Base
+ include Gitlab::Kubernetes
+
self.table_name = 'clusters_kubernetes_namespaces'
belongs_to :cluster_project, class_name: 'Clusters::Project'
@@ -12,25 +14,40 @@ module Clusters
validates :namespace, presence: true
validates :namespace, uniqueness: { scope: :cluster_id }
- before_validation :set_namespace_and_service_account_to_default, on: :create
+ delegate :ca_pem, to: :platform_kubernetes, allow_nil: true
+ delegate :api_url, to: :platform_kubernetes, allow_nil: true
attr_encrypted :service_account_token,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-cbc'
+ scope :has_service_account_token, -> { where.not(encrypted_service_account_token: nil) }
+
def token_name
"#{namespace}-token"
end
- private
+ def configure_predefined_credentials
+ self.namespace = kubernetes_or_project_namespace
+ self.service_account_name = default_service_account_name
+ end
+
+ def predefined_variables
+ config = YAML.dump(kubeconfig)
- def set_namespace_and_service_account_to_default
- self.namespace ||= default_namespace
- self.service_account_name ||= default_service_account_name
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ variables
+ .append(key: 'KUBE_SERVICE_ACCOUNT', value: service_account_name)
+ .append(key: 'KUBE_NAMESPACE', value: namespace)
+ .append(key: 'KUBE_TOKEN', value: service_account_token, public: false)
+ .append(key: 'KUBECONFIG', value: config, public: false, file: true)
+ end
end
- def default_namespace
+ private
+
+ def kubernetes_or_project_namespace
platform_kubernetes&.namespace.presence || project_namespace
end
@@ -45,5 +62,13 @@ module Clusters
def project_slug
"#{project.path}-#{project.id}".downcase
end
+
+ def kubeconfig
+ to_kubeconfig(
+ url: api_url,
+ namespace: namespace,
+ token: service_account_token,
+ ca_pem: ca_pem)
+ end
end
end
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index f0f791742f4..3c5d7756eec 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -6,6 +6,7 @@ module Clusters
include Gitlab::Kubernetes
include ReactiveCaching
include EnumWithNil
+ include AfterCommitQueue
RESERVED_NAMESPACES = %w(gitlab-managed-apps).freeze
@@ -25,6 +26,7 @@ module Clusters
algorithm: 'aes-256-cbc'
before_validation :enforce_namespace_to_lower_case
+ before_validation :enforce_ca_whitespace_trimming
validates :namespace,
allow_blank: true,
@@ -36,6 +38,8 @@ module Clusters
validates :namespace, exclusion: { in: RESERVED_NAMESPACES }
+ validate :no_namespace, unless: :allow_user_defined_namespace?
+
# We expect to be `active?` only when enabled and cluster is created (the api_url is assigned)
validates :api_url, url: true, presence: true
validates :token, presence: true
@@ -43,12 +47,14 @@ module Clusters
validate :prevent_modification, on: :update
after_save :clear_reactive_cache!
+ after_update :update_kubernetes_namespace
alias_attribute :ca_pem, :ca_cert
delegate :project, to: :cluster, allow_nil: true
delegate :enabled?, to: :cluster, allow_nil: true
delegate :managed?, to: :cluster, allow_nil: true
+ delegate :allow_user_defined_namespace?, to: :cluster, allow_nil: true
delegate :kubernetes_namespace, to: :cluster
alias_method :active?, :enabled?
@@ -67,21 +73,31 @@ module Clusters
end
end
- def predefined_variables
- config = YAML.dump(kubeconfig)
-
+ def predefined_variables(project:)
Gitlab::Ci::Variables::Collection.new.tap do |variables|
- variables
- .append(key: 'KUBE_URL', value: api_url)
- .append(key: 'KUBE_TOKEN', value: token, public: false)
- .append(key: 'KUBE_NAMESPACE', value: actual_namespace)
- .append(key: 'KUBECONFIG', value: config, public: false, file: true)
+ variables.append(key: 'KUBE_URL', value: api_url)
if ca_pem.present?
variables
.append(key: 'KUBE_CA_PEM', value: ca_pem)
.append(key: 'KUBE_CA_PEM_FILE', value: ca_pem, file: true)
end
+
+ if kubernetes_namespace = cluster.kubernetes_namespaces.has_service_account_token.find_by(project: project)
+ variables.concat(kubernetes_namespace.predefined_variables)
+ else
+ # 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
+ config = YAML.dump(kubeconfig)
+
+ variables
+ .append(key: 'KUBE_URL', value: api_url)
+ .append(key: 'KUBE_TOKEN', value: token, public: false)
+ .append(key: 'KUBE_NAMESPACE', value: actual_namespace)
+ .append(key: 'KUBECONFIG', value: config, public: false, file: true)
+ end
end
end
@@ -137,7 +153,8 @@ module Clusters
end
def build_kube_client!
- raise "Incomplete settings" unless api_url && actual_namespace
+ raise "Incomplete settings" unless api_url
+ raise "No namespace" if cluster.project_type? && actual_namespace.empty? # can probably remove this line once we remove #actual_namespace
unless (username && password) || token
raise "Either username/password or token is required to access API"
@@ -156,9 +173,7 @@ module Clusters
kubeclient = build_kube_client!
kubeclient.get_pods(namespace: actual_namespace).as_json
- rescue Kubeclient::HttpError => err
- raise err unless err.error_code == 404
-
+ rescue Kubeclient::ResourceNotFoundError
[]
end
@@ -189,6 +204,17 @@ module Clusters
self.namespace = self.namespace&.downcase
end
+ def enforce_ca_whitespace_trimming
+ self.ca_pem = self.ca_pem&.strip
+ self.token = self.token&.strip
+ end
+
+ def no_namespace
+ if namespace
+ errors.add(:namespace, 'only allowed for project cluster')
+ end
+ end
+
def prevent_modification
return unless managed?
@@ -199,6 +225,14 @@ module Clusters
true
end
+
+ def update_kubernetes_namespace
+ return unless namespace_changed?
+
+ run_after_commit do
+ ClusterPlatformConfigureWorker.perform_async(cluster_id)
+ end
+ end
end
end
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index a61ed03cf35..9dd0cbacd9e 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -260,7 +260,7 @@ class Commit
request_cache(:author) { author_email.downcase }
def committer
- @committer ||= User.find_by_any_email(committer_email.downcase)
+ @committer ||= User.find_by_any_email(committer_email)
end
def parents
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 7d36f9982ab..755f8bd4d06 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -51,7 +51,8 @@ class CommitStatus < ActiveRecord::Base
missing_dependency_failure: 5,
runner_unsupported: 6,
stale_schedule: 7,
- job_execution_timeout: 8
+ job_execution_timeout: 8,
+ archived_failure: 9
}
##
@@ -167,16 +168,18 @@ class CommitStatus < ActiveRecord::Base
false
end
- # To be overriden when inherrited from
def retryable?
false
end
- # To be overriden when inherrited from
def cancelable?
false
end
+ def archived?
+ false
+ end
+
def stuck?
false
end
diff --git a/app/models/compare.rb b/app/models/compare.rb
index b2d46ada831..f1ed84ab5a5 100644
--- a/app/models/compare.rb
+++ b/app/models/compare.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require 'set'
+
class Compare
include Gitlab::Utils::StrongMemoize
@@ -77,4 +79,13 @@ class Compare
head_sha: head_commit_sha
)
end
+
+ def modified_paths
+ paths = Set.new
+ diffs.diff_files.each do |diff|
+ paths.add diff.old_path
+ paths.add diff.new_path
+ end
+ paths.to_a
+ end
end
diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb
index 6f29c92d176..60b7ec2815c 100644
--- a/app/models/concerns/awardable.rb
+++ b/app/models/concerns/awardable.rb
@@ -13,13 +13,13 @@ module Awardable
end
class_methods do
- def awarded(user, name)
+ def awarded(user, name = nil)
sql = <<~EOL
EXISTS (
SELECT TRUE
FROM award_emoji
WHERE user_id = :user_id AND
- name = :name AND
+ #{"name = :name AND" if name.present?}
awardable_type = :awardable_type AND
awardable_id = #{self.arel_table.name}.id
)
@@ -28,6 +28,20 @@ module Awardable
where(sql, user_id: user.id, name: name, awardable_type: self.name)
end
+ def not_awarded(user)
+ sql = <<~EOL
+ NOT EXISTS (
+ SELECT TRUE
+ FROM award_emoji
+ WHERE user_id = :user_id AND
+ awardable_type = :awardable_type AND
+ awardable_id = #{self.arel_table.name}.id
+ )
+ EOL
+
+ where(sql, user_id: user.id, awardable_type: self.name)
+ end
+
def order_upvotes_desc
order_votes_desc(AwardEmoji::UPVOTE_NAME)
end
diff --git a/app/models/concerns/blob_language_from_git_attributes.rb b/app/models/concerns/blob_language_from_git_attributes.rb
new file mode 100644
index 00000000000..70213d22147
--- /dev/null
+++ b/app/models/concerns/blob_language_from_git_attributes.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+# Applicable for blob classes with project attribute
+module BlobLanguageFromGitAttributes
+ extend ActiveSupport::Concern
+
+ def language_from_gitattributes
+ return nil unless project
+
+ repository = project.repository
+ repository.gitattribute(path, 'gitlab-language')
+ end
+end
diff --git a/app/models/concerns/cacheable_attributes.rb b/app/models/concerns/cacheable_attributes.rb
index f8034be8376..75592bb63e2 100644
--- a/app/models/concerns/cacheable_attributes.rb
+++ b/app/models/concerns/cacheable_attributes.rb
@@ -12,12 +12,12 @@ module CacheableAttributes
"#{name}:#{Gitlab::VERSION}:#{Rails.version}".freeze
end
- # Can be overriden
+ # Can be overridden
def current_without_cache
last
end
- # Can be overriden
+ # Can be overridden
def defaults
{}
end
diff --git a/app/models/concerns/deployable.rb b/app/models/concerns/deployable.rb
new file mode 100644
index 00000000000..bc12b06b5af
--- /dev/null
+++ b/app/models/concerns/deployable.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Deployable
+ extend ActiveSupport::Concern
+
+ included do
+ after_create :create_deployment
+
+ def create_deployment
+ return unless starts_environment? && !has_deployment?
+
+ environment = project.environments.find_or_create_by(
+ name: expanded_environment_name
+ )
+
+ # If we failed to persist envirionment record by validation error, such as name with invalid character,
+ # the job will fall back to a non-environment job.
+ return unless environment.persisted?
+
+ create_deployment!(
+ project_id: environment.project_id,
+ environment: environment,
+ ref: ref,
+ tag: tag,
+ sha: sha,
+ user: user,
+ on_stop: on_stop)
+ end
+ end
+end
diff --git a/app/models/concerns/each_batch.rb b/app/models/concerns/each_batch.rb
index 8cf0b8b154d..6314b46a7e3 100644
--- a/app/models/concerns/each_batch.rb
+++ b/app/models/concerns/each_batch.rb
@@ -39,7 +39,15 @@ module EachBatch
#
# of - The number of rows to retrieve per batch.
# column - The column to use for ordering the batches.
- def each_batch(of: 1000, column: primary_key)
+ # order_hint - An optional column to append to the `ORDER BY id`
+ # clause to help the query planner. PostgreSQL might perform badly
+ # with a LIMIT 1 because the planner is guessing that scanning the
+ # index in ID order will come across the desired row in less time
+ # it will take the planner than using another index. The
+ # order_hint does not affect the search results. For example,
+ # `ORDER BY id ASC, updated_at ASC` means the same thing as `ORDER
+ # BY id ASC`.
+ def each_batch(of: 1000, column: primary_key, order_hint: nil)
unless column
raise ArgumentError,
'the column: argument must be set to a column name to use for ordering rows'
@@ -48,7 +56,9 @@ module EachBatch
start = except(:select)
.select(column)
.reorder(column => :asc)
- .take
+
+ start = start.order(order_hint) if order_hint
+ start = start.take
return unless start
@@ -60,6 +70,9 @@ module EachBatch
.select(column)
.where(arel_table[column].gteq(start_id))
.reorder(column => :asc)
+
+ stop = stop.order(order_hint) if order_hint
+ stop = stop
.offset(of)
.limit(1)
.take
diff --git a/app/models/concerns/fast_destroy_all.rb b/app/models/concerns/fast_destroy_all.rb
index c342d01243e..2bfa7da6c1c 100644
--- a/app/models/concerns/fast_destroy_all.rb
+++ b/app/models/concerns/fast_destroy_all.rb
@@ -7,7 +7,7 @@
# `delete_all` is efficient as it deletes all rows with a single `DELETE` query.
#
# It's better to use `delete_all` as our best practice, however,
-# if external data (e.g. ObjectStorage, FileStorage or Redis) are assosiated with database records,
+# if external data (e.g. ObjectStorage, FileStorage or Redis) are associated with database records,
# it is difficult to accomplish it.
#
# This module defines a format to use `delete_all` and delete associated external data.
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index a808f9ad4ad..69c5affe142 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -363,7 +363,7 @@ module Issuable
end
##
- # Overriden in MergeRequest
+ # Overridden in MergeRequest
#
def wipless_title_changed(old_title)
old_title != title
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index 298d0d42d90..0d88b34fb48 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -97,9 +97,9 @@ module Mentionable
# Allows heavy processing to be skipped
def matches_cross_reference_regex?
reference_pattern = if !project || project.default_issues_tracker?
- ReferenceRegexes::DEFAULT_PATTERN
+ ReferenceRegexes.default_pattern
else
- ReferenceRegexes::EXTERNAL_PATTERN
+ ReferenceRegexes.external_pattern
end
self.class.mentionable_attrs.any? do |attr, _|
diff --git a/app/models/concerns/mentionable/reference_regexes.rb b/app/models/concerns/mentionable/reference_regexes.rb
index fe8fbb71184..b8fb3f71925 100644
--- a/app/models/concerns/mentionable/reference_regexes.rb
+++ b/app/models/concerns/mentionable/reference_regexes.rb
@@ -2,6 +2,8 @@
module Mentionable
module ReferenceRegexes
+ extend Gitlab::Utils::StrongMemoize
+
def self.reference_pattern(link_patterns, issue_pattern)
Regexp.union(link_patterns,
issue_pattern,
@@ -15,16 +17,20 @@ module Mentionable
]
end
- DEFAULT_PATTERN = begin
- issue_pattern = Issue.reference_pattern
- link_patterns = Regexp.union([Issue, Commit, MergeRequest, Epic].map(&:link_reference_pattern).compact)
- reference_pattern(link_patterns, issue_pattern)
+ def self.default_pattern
+ strong_memoize(:default_pattern) do
+ issue_pattern = Issue.reference_pattern
+ link_patterns = Regexp.union([Issue, Commit, MergeRequest, Epic].map(&:link_reference_pattern).compact)
+ reference_pattern(link_patterns, issue_pattern)
+ end
end
- EXTERNAL_PATTERN = begin
- issue_pattern = IssueTrackerService.reference_pattern
- link_patterns = URI.regexp(%w(http https))
- reference_pattern(link_patterns, issue_pattern)
+ def self.external_pattern
+ strong_memoize(:external_pattern) do
+ issue_pattern = IssueTrackerService.reference_pattern
+ link_patterns = URI.regexp(%w(http https))
+ reference_pattern(link_patterns, issue_pattern)
+ end
end
end
end
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index 098eed137ba..eb315058c3a 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -23,7 +23,7 @@ module Noteable
end
def supports_discussions?
- DiscussionNote::NOTEABLE_TYPES.include?(base_class_name)
+ DiscussionNote.noteable_types.include?(base_class_name)
end
def discussions_rendered_on_frontend?
diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb
index 7723c07279d..af699eeebce 100644
--- a/app/models/concerns/storage/legacy_namespace.rb
+++ b/app/models/concerns/storage/legacy_namespace.rb
@@ -94,7 +94,7 @@ module Storage
if gitlab_shell.mv_namespace(repository_storage, full_path, new_path)
Gitlab::AppLogger.info %Q(Namespace directory "#{full_path}" moved to "#{new_path}")
- # Remove namespace directroy async with delay so
+ # Remove namespace directory async with delay so
# GitLab has time to remove all projects first
run_after_commit do
GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage, new_path)
diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb
index 66db4bd92de..23a43aec677 100644
--- a/app/models/concerns/token_authenticatable.rb
+++ b/app/models/concerns/token_authenticatable.rb
@@ -10,6 +10,7 @@ module TokenAuthenticatable
def add_authentication_token_field(token_field, options = {})
@token_fields = [] unless @token_fields
+ unique = options.fetch(:unique, true)
if @token_fields.include?(token_field)
raise ArgumentError.new("#{token_field} already configured via add_authentication_token_field")
@@ -25,8 +26,10 @@ module TokenAuthenticatable
TokenAuthenticatableStrategies::Insecure.new(self, token_field, options)
end
- define_singleton_method("find_by_#{token_field}") do |token|
- strategy.find_token_authenticatable(token)
+ if unique
+ define_singleton_method("find_by_#{token_field}") do |token|
+ strategy.find_token_authenticatable(token)
+ end
end
define_method(token_field) do
diff --git a/app/models/concerns/token_authenticatable_strategies/base.rb b/app/models/concerns/token_authenticatable_strategies/base.rb
index f0f7107d627..413721d3e6c 100644
--- a/app/models/concerns/token_authenticatable_strategies/base.rb
+++ b/app/models/concerns/token_authenticatable_strategies/base.rb
@@ -43,10 +43,14 @@ module TokenAuthenticatableStrategies
set_token(instance, new_token)
end
+ def unique
+ @options.fetch(:unique, true)
+ end
+
def generate_available_token
loop do
token = generate_token
- break token unless find_token_authenticatable(token, true)
+ break token unless unique && find_token_authenticatable(token, true)
end
end
diff --git a/app/models/concerns/with_uploads.rb b/app/models/concerns/with_uploads.rb
index e231af5368d..2bdef2a40e4 100644
--- a/app/models/concerns/with_uploads.rb
+++ b/app/models/concerns/with_uploads.rb
@@ -2,7 +2,7 @@
# Mounted uploaders are destroyed by carrierwave's after_commit
# hook. This hook fetches upload location (local vs remote) from
-# Upload model. So it's neccessary to make sure that during that
+# Upload model. So it's necessary to make sure that during that
# after_commit hook model's associated uploads are not deleted yet.
# IOW we can not use dependent: :destroy :
# has_many :uploads, as: :model, dependent: :destroy
diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb
index 0b2eedf3631..e3524305346 100644
--- a/app/models/deploy_token.rb
+++ b/app/models/deploy_token.rb
@@ -4,6 +4,7 @@ class DeployToken < ActiveRecord::Base
include Expirable
include TokenAuthenticatable
include PolicyActor
+ include Gitlab::Utils::StrongMemoize
add_authentication_token_field :token
AVAILABLE_SCOPES = %i(read_repository read_registry).freeze
@@ -49,7 +50,9 @@ class DeployToken < ActiveRecord::Base
# to a single project, later we're going to extend
# that to be for multiple projects and namespaces.
def project
- projects.first
+ strong_memoize(:project) do
+ projects.first
+ end
end
def expires_at
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index ee5b96e7454..811e623b7f7 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -3,24 +3,60 @@
class Deployment < ActiveRecord::Base
include AtomicInternalId
include IidRoutes
+ include AfterCommitQueue
belongs_to :project, required: true
belongs_to :environment, required: true
belongs_to :user
belongs_to :deployable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
- has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.deployments&.maximum(:iid) }
+ has_internal_id :iid, scope: :project, init: ->(s) do
+ Deployment.where(project: s.project).maximum(:iid) if s&.project
+ end
validates :sha, presence: true
validates :ref, presence: true
delegate :name, to: :environment, prefix: true
- after_create :create_ref
- after_create :invalidate_cache
-
scope :for_environment, -> (environment) { where(environment_id: environment) }
+ state_machine :status, initial: :created do
+ event :run do
+ transition created: :running
+ end
+
+ event :succeed do
+ transition any - [:success] => :success
+ end
+
+ event :drop do
+ transition any - [:failed] => :failed
+ end
+
+ event :cancel do
+ transition any - [:canceled] => :canceled
+ end
+
+ before_transition any => [:success, :failed, :canceled] do |deployment|
+ deployment.finished_at = Time.now
+ end
+
+ after_transition any => :success do |deployment|
+ deployment.run_after_commit do
+ Deployments::SuccessWorker.perform_async(id)
+ end
+ end
+ end
+
+ enum status: {
+ created: 0,
+ running: 1,
+ success: 2,
+ failed: 3,
+ canceled: 4
+ }
+
def self.last_for_environment(environment)
ids = self
.for_environment(environment)
@@ -55,7 +91,11 @@ class Deployment < ActiveRecord::Base
end
def manual_actions
- @manual_actions ||= deployable.try(:other_actions)
+ @manual_actions ||= deployable.try(:other_manual_actions)
+ end
+
+ def scheduled_actions
+ @scheduled_actions ||= deployable.try(:other_scheduled_actions)
end
def includes_commit?(commit)
@@ -65,15 +105,15 @@ class Deployment < ActiveRecord::Base
end
def update_merge_request_metrics!
- return unless environment.update_merge_request_metrics?
+ return unless environment.update_merge_request_metrics? && success?
merge_requests = project.merge_requests
.joins(:metrics)
.where(target_branch: self.ref, merge_request_metrics: { first_deployed_to_production_at: nil })
- .where("merge_request_metrics.merged_at <= ?", self.created_at)
+ .where("merge_request_metrics.merged_at <= ?", finished_at)
if previous_deployment
- merge_requests = merge_requests.where("merge_request_metrics.merged_at >= ?", previous_deployment.created_at)
+ merge_requests = merge_requests.where("merge_request_metrics.merged_at >= ?", previous_deployment.finished_at)
end
# Need to use `map` instead of `select` because MySQL doesn't allow `SELECT`ing from the same table
@@ -87,7 +127,7 @@ class Deployment < ActiveRecord::Base
MergeRequest::Metrics
.where(merge_request_id: merge_request_ids, first_deployed_to_production_at: nil)
- .update_all(first_deployed_to_production_at: self.created_at)
+ .update_all(first_deployed_to_production_at: finished_at)
end
def previous_deployment
@@ -105,30 +145,36 @@ class Deployment < ActiveRecord::Base
@stop_action ||= manual_actions.find_by(name: on_stop)
end
+ def finished_at
+ read_attribute(:finished_at) || legacy_finished_at
+ end
+
+ def deployed_at
+ return unless success?
+
+ finished_at
+ end
+
def formatted_deployment_time
- created_at.to_time.in_time_zone.to_s(:medium)
+ deployed_at&.to_time&.in_time_zone&.to_s(:medium)
end
def has_metrics?
- prometheus_adapter&.can_query?
+ prometheus_adapter&.can_query? && success?
end
def metrics
return {} unless has_metrics?
metrics = prometheus_adapter.query(:deployment, self)
- metrics&.merge(deployment_time: created_at.to_i) || {}
+ metrics&.merge(deployment_time: finished_at.to_i) || {}
end
def additional_metrics
return {} unless has_metrics?
metrics = prometheus_adapter.query(:additional_metrics_deployment, self)
- metrics&.merge(deployment_time: created_at.to_i) || {}
- end
-
- def status
- 'success'
+ metrics&.merge(deployment_time: finished_at.to_i) || {}
end
private
@@ -140,4 +186,8 @@ class Deployment < ActiveRecord::Base
def ref_path
File.join(environment.ref_path, 'deployments', iid.to_s)
end
+
+ def legacy_finished_at
+ self.created_at if success? && !read_attribute(:finished_at)
+ end
end
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index 95694377fe3..c32008aa9c7 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -8,12 +8,14 @@ class DiffNote < Note
include DiffPositionableNote
include Gitlab::Utils::StrongMemoize
- NOTEABLE_TYPES = %w(MergeRequest Commit).freeze
+ def self.noteable_types
+ %w(MergeRequest Commit)
+ end
validates :original_position, presence: true
validates :position, presence: true
validates :line_code, presence: true, line_code: true, if: :on_text?
- validates :noteable_type, inclusion: { in: NOTEABLE_TYPES }
+ validates :noteable_type, inclusion: { in: noteable_types }
validate :positions_complete
validate :verify_supported
validate :diff_refs_match_commit, if: :for_commit?
@@ -64,6 +66,10 @@ class DiffNote < Note
self.original_position.diff_refs == diff_refs
end
+ def discussion_first_note?
+ self == discussion.first_note
+ end
+
private
def enqueue_diff_file_creation_job
@@ -76,26 +82,33 @@ class DiffNote < Note
end
def should_create_diff_file?
- on_text? && note_diff_file.nil? && self == discussion.first_note
+ on_text? && note_diff_file.nil? && discussion_first_note?
end
def fetch_diff_file
- if note_diff_file
- diff = Gitlab::Git::Diff.new(note_diff_file.to_hash)
- Gitlab::Diff::File.new(diff,
- repository: project.repository,
- diff_refs: original_position.diff_refs)
- elsif created_at_diff?(noteable.diff_refs)
- # We're able to use the already persisted diffs (Postgres) if we're
- # presenting a "current version" of the MR discussion diff.
- # So no need to make an extra Gitaly diff request for it.
- # As an extra benefit, the returned `diff_file` already
- # has `highlighted_diff_lines` data set from Redis on
- # `Diff::FileCollection::MergeRequestDiff`.
- noteable.diffs(original_position.diff_options).diff_files.first
- else
- original_position.diff_file(self.project.repository)
- end
+ file =
+ if note_diff_file
+ diff = Gitlab::Git::Diff.new(note_diff_file.to_hash)
+ Gitlab::Diff::File.new(diff,
+ repository: project.repository,
+ diff_refs: original_position.diff_refs)
+ elsif created_at_diff?(noteable.diff_refs)
+ # We're able to use the already persisted diffs (Postgres) if we're
+ # presenting a "current version" of the MR discussion diff.
+ # So no need to make an extra Gitaly diff request for it.
+ # As an extra benefit, the returned `diff_file` already
+ # has `highlighted_diff_lines` data set from Redis on
+ # `Diff::FileCollection::MergeRequestDiff`.
+ noteable.diffs(original_position.diff_options).diff_files.first
+ else
+ original_position.diff_file(self.project.repository)
+ end
+
+ # Since persisted diff files already have its content "unfolded"
+ # there's no need to make it pass through the unfolding process.
+ file&.unfold_diff_lines(position) unless note_diff_file
+
+ file
end
def supported?
diff --git a/app/models/discussion_note.rb b/app/models/discussion_note.rb
index 89d86aaed66..142cbdcdfa6 100644
--- a/app/models/discussion_note.rb
+++ b/app/models/discussion_note.rb
@@ -5,9 +5,11 @@
# A note of this type can be resolvable.
class DiscussionNote < Note
# Names of all implementers of `Noteable` that support discussions.
- NOTEABLE_TYPES = %w(MergeRequest Issue Commit Snippet).freeze
+ def self.noteable_types
+ %w(MergeRequest Issue Commit Snippet)
+ end
- validates :noteable_type, inclusion: { in: NOTEABLE_TYPES }
+ validates :noteable_type, inclusion: { in: noteable_types }
def discussion_class(*)
Discussion
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 1c31c01eb9f..934828946b9 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -8,9 +8,9 @@ class Environment < ActiveRecord::Base
belongs_to :project, required: true
- has_many :deployments, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :deployments, -> { success }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_one :last_deployment, -> { order('deployments.id DESC') }, class_name: 'Deployment'
+ has_one :last_deployment, -> { success.order('deployments.id DESC') }, class_name: 'Deployment'
before_validation :nullify_external_url
before_validation :generate_slug, if: ->(env) { env.slug.blank? }
@@ -50,6 +50,7 @@ class Environment < ActiveRecord::Base
scope :in_review_folder, -> { where(environment_type: "review") }
scope :for_name, -> (name) { where(name: name) }
scope :for_project, -> (project) { where(project_id: project) }
+ scope :with_deployment, -> (sha) { where('EXISTS (?)', Deployment.select(1).where('deployments.environment_id = environments.id').where(sha: sha)) }
state_machine :state, initial: :available do
event :start do
diff --git a/app/models/environment_status.rb b/app/models/environment_status.rb
index a84871f7253..7078496ff52 100644
--- a/app/models/environment_status.rb
+++ b/app/models/environment_status.rb
@@ -9,16 +9,15 @@ class EnvironmentStatus
delegate :name, to: :environment
delegate :project, to: :environment
delegate :deployed_at, to: :deployment, allow_nil: true
- delegate :status, to: :deployment
def self.for_merge_request(mr, user)
- build_environments_status(mr, user, mr.head_pipeline)
+ build_environments_status(mr, user, mr.diff_head_sha)
end
def self.after_merge_request(mr, user)
return [] unless mr.merged?
- build_environments_status(mr, user, mr.merge_pipeline)
+ build_environments_status(mr, user, mr.merge_commit_sha)
end
def initialize(environment, merge_request, sha)
@@ -29,14 +28,10 @@ class EnvironmentStatus
def deployment
strong_memoize(:deployment) do
- environment.first_deployment_for(sha)
+ Deployment.where(environment: environment).find_by_sha(sha)
end
end
- def deployed_at
- deployment&.created_at
- end
-
def changes
return [] if project.route_map_for(sha).nil?
@@ -48,6 +43,22 @@ class EnvironmentStatus
.merge_request_diff_files.where(deleted_file: false)
end
+ ##
+ # Since frontend has not supported all statuses yet, BE has to
+ # proxy some status to a supported status.
+ def status
+ return unless deployment
+
+ case deployment.status
+ when 'created'
+ 'running'
+ when 'canceled'
+ 'failed'
+ else
+ deployment.status
+ end
+ end
+
private
PAGE_EXTENSIONS = /\A\.(s?html?|php|asp|cgi|pl)\z/i.freeze
@@ -65,21 +76,14 @@ class EnvironmentStatus
}
end
- def self.build_environments_status(mr, user, pipeline)
- return [] unless pipeline.present?
+ def self.build_environments_status(mr, user, sha)
+ Environment.where(project_id: [mr.source_project_id, mr.target_project_id])
+ .available
+ .with_deployment(sha).map do |environment|
+ next unless Ability.allowed?(user, :read_environment, environment)
- find_environments(user, pipeline).map do |environment|
- EnvironmentStatus.new(environment, mr, pipeline.sha)
- end
+ EnvironmentStatus.new(environment, mr, sha)
+ end.compact
end
private_class_method :build_environments_status
-
- def self.find_environments(user, pipeline)
- env_ids = Deployment.where(deployable: pipeline.builds).select(:environment_id)
-
- Environment.available.where(id: env_ids).select do |environment|
- Ability.allowed?(user, :read_environment, environment)
- end
- end
- private_class_method :find_environments
end
diff --git a/app/models/identity.rb b/app/models/identity.rb
index f5a13dbd6f2..d63dd432426 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -1,18 +1,14 @@
# frozen_string_literal: true
class Identity < ActiveRecord::Base
- def self.uniqueness_scope
- :provider
- end
-
include Sortable
include CaseSensitivity
belongs_to :user
validates :provider, presence: true
- validates :extern_uid, allow_blank: true, uniqueness: { scope: uniqueness_scope, case_sensitive: false }
- validates :user_id, uniqueness: { scope: uniqueness_scope }
+ validates :extern_uid, allow_blank: true, uniqueness: { scope: UniquenessScopes.scopes, case_sensitive: false }
+ validates :user_id, uniqueness: { scope: UniquenessScopes.scopes }
before_save :ensure_normalized_extern_uid, if: :extern_uid_changed?
after_destroy :clear_user_synced_attributes, if: :user_synced_attributes_metadata_from_provider?
diff --git a/app/models/identity/uniqueness_scopes.rb b/app/models/identity/uniqueness_scopes.rb
new file mode 100644
index 00000000000..674b735903f
--- /dev/null
+++ b/app/models/identity/uniqueness_scopes.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class Identity < ActiveRecord::Base
+ # This module and method are defined in a separate file to allow EE to
+ # redefine the `scopes` method before it is used in the `Identity` model.
+ module UniquenessScopes
+ def self.scopes
+ [:provider]
+ end
+ end
+end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 4ace5d3ab97..0de5e434b02 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -240,7 +240,8 @@ class Issue < ActiveRecord::Base
reference_path: issue_reference,
real_path: url_helper.project_issue_path(project, self),
issue_sidebar_endpoint: url_helper.project_issue_path(project, self, format: :json, serializer: 'sidebar'),
- toggle_subscription_endpoint: url_helper.toggle_subscription_project_issue_path(project, self)
+ toggle_subscription_endpoint: url_helper.toggle_subscription_project_issue_path(project, self),
+ assignable_labels_endpoint: url_helper.project_labels_path(project, format: :json, include_ancestor_groups: true)
)
end
diff --git a/app/models/key.rb b/app/models/key.rb
index bdb83e12793..8f93418b88b 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -34,6 +34,10 @@ class Key < ActiveRecord::Base
after_destroy :post_destroy_hook
after_destroy :refresh_user_cache
+ def self.regular_keys
+ where(type: ['Key', nil])
+ end
+
def key=(value)
write_attribute(:key, value.present? ? Gitlab::SSHPublicKey.sanitize(value) : nil)
diff --git a/app/models/label.rb b/app/models/label.rb
index 43b49445765..165e4a8f3e5 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -41,8 +41,8 @@ class Label < ActiveRecord::Base
scope :templates, -> { where(template: true) }
scope :with_title, ->(title) { where(title: title) }
scope :with_lists_and_board, -> { joins(lists: :board).merge(List.movable) }
- scope :on_group_boards, ->(group_id) { with_lists_and_board.where(boards: { group_id: group_id }) }
scope :on_project_boards, ->(project_id) { with_lists_and_board.where(boards: { project_id: project_id }) }
+ scope :on_board, ->(board_id) { with_lists_and_board.where(boards: { id: board_id }) }
scope :order_name_asc, -> { reorder(title: :asc) }
scope :order_name_desc, -> { reorder(title: :desc) }
scope :subscribed_by, ->(user_id) { joins(:subscriptions).where(subscriptions: { user_id: user_id, subscribed: true }) }
diff --git a/app/models/members_preloader.rb b/app/models/members_preloader.rb
new file mode 100644
index 00000000000..33855191ca8
--- /dev/null
+++ b/app/models/members_preloader.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class MembersPreloader
+ attr_reader :members
+
+ def initialize(members)
+ @members = members
+ end
+
+ def preload_all
+ ActiveRecord::Associations::Preloader.new.preload(members, :user)
+ ActiveRecord::Associations::Preloader.new.preload(members, :source)
+ ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :status)
+ ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :u2f_registrations)
+ end
+end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 7eef08aa6a3..df5678ec2f1 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -353,6 +353,15 @@ class MergeRequest < ActiveRecord::Base
end
end
+ # Returns true if there are commits that match at least one commit SHA.
+ def includes_any_commits?(shas)
+ if persisted?
+ merge_request_diff.commits_by_shas(shas).exists?
+ else
+ (commit_shas & shas).present?
+ end
+ end
+
# Calls `MergeWorker` to proceed with the merge process and
# updates `merge_jid` with the MergeWorker#jid.
# This helps tracking enqueued and ongoing merge jobs.
@@ -400,6 +409,18 @@ class MergeRequest < ActiveRecord::Base
merge_request_diff&.real_size || diffs.real_size
end
+ def modified_paths(past_merge_request_diff: nil)
+ diffs = if past_merge_request_diff
+ past_merge_request_diff
+ elsif compare
+ compare
+ else
+ self.merge_request_diff
+ end
+
+ diffs.modified_paths
+ end
+
def diff_base_commit
if persisted?
merge_request_diff.base_commit
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 02c6b650f33..6f1beede6f9 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -6,6 +6,7 @@ class MergeRequestDiff < ActiveRecord::Base
include ManualInverseAssociation
include IgnorableColumn
include EachBatch
+ include Gitlab::Utils::StrongMemoize
# Don't display more than 100 commits at once
COMMITS_SAFE_SIZE = 100
@@ -140,6 +141,12 @@ class MergeRequestDiff < ActiveRecord::Base
merge_request_diff_commits.map(&:sha)
end
+ def commits_by_shas(shas)
+ return MergeRequestDiffCommit.none unless shas.present?
+
+ merge_request_diff_commits.where(sha: shas)
+ end
+
def diff_refs=(new_diff_refs)
self.base_commit_sha = new_diff_refs&.base_sha
self.start_commit_sha = new_diff_refs&.start_sha
@@ -228,6 +235,12 @@ class MergeRequestDiff < ActiveRecord::Base
end
# rubocop: enable CodeReuse/ServiceClass
+ def modified_paths
+ strong_memoize(:modified_paths) do
+ merge_request_diff_files.pluck(:new_path, :old_path).flatten.uniq
+ end
+ end
+
private
def create_merge_request_diff_files(diffs)
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 599bedde27d..4a6627d3ca1 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -83,7 +83,7 @@ class Namespace < ActiveRecord::Base
find_by('lower(path) = :value', value: path.downcase)
end
- # Case insensetive search for namespace by path or name
+ # Case insensitive search for namespace by path or name
def find_by_path_or_name(path)
find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase)
end
@@ -232,6 +232,12 @@ class Namespace < ActiveRecord::Base
Project.inside_path(full_path)
end
+ # Includes pipelines from this namespace and pipelines from all subgroups
+ # that belongs to this namespace
+ def all_pipelines
+ Ci::Pipeline.where(project: all_projects)
+ end
+
def has_parent?
parent.present?
end
diff --git a/app/models/note.rb b/app/models/note.rb
index 990689a95f5..592efb714f3 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -117,6 +117,8 @@ class Note < ActiveRecord::Base
case notes_filter
when UserPreference::NOTES_FILTERS[:only_comments]
user
+ when UserPreference::NOTES_FILTERS[:only_activity]
+ system
else
all
end
diff --git a/app/models/pool_repository.rb b/app/models/pool_repository.rb
new file mode 100644
index 00000000000..8ef74539209
--- /dev/null
+++ b/app/models/pool_repository.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class PoolRepository < ActiveRecord::Base
+ POOL_PREFIX = '@pools'
+
+ belongs_to :shard
+ validates :shard, presence: true
+
+ # For now, only pool repositories are tracked in the database. However, we may
+ # want to add other repository types in the future
+ self.table_name = 'repositories'
+
+ has_many :pool_member_projects, class_name: 'Project', foreign_key: :pool_repository_id
+
+ def shard_name
+ shard&.name
+ end
+
+ def shard_name=(name)
+ self.shard = Shard.by_name(name)
+ end
+end
diff --git a/app/models/postgresql/replication_slot.rb b/app/models/postgresql/replication_slot.rb
index 70c7432e6b5..e264fe88e47 100644
--- a/app/models/postgresql/replication_slot.rb
+++ b/app/models/postgresql/replication_slot.rb
@@ -4,6 +4,15 @@ module Postgresql
class ReplicationSlot < ActiveRecord::Base
self.table_name = 'pg_replication_slots'
+ # Returns true if there are any replication slots in use.
+ # PostgreSQL-compatible databases such as Aurora don't support
+ # replication slots, so this will return false as well.
+ def self.in_use?
+ transaction { exists? }
+ rescue ActiveRecord::StatementInvalid
+ false
+ end
+
# Returns true if the lag observed across all replication slots exceeds a
# given threshold.
#
@@ -11,6 +20,8 @@ module Postgresql
# statistics it takes between 1 and 5 seconds to replicate around
# 100 MB of data.
def self.lag_too_great?(max = 100.megabytes)
+ return false unless in_use?
+
lag_function = "#{Gitlab::Database.pg_wal_lsn_diff}" \
"(#{Gitlab::Database.pg_current_wal_insert_lsn}(), restart_lsn)::bigint"
diff --git a/app/models/project.rb b/app/models/project.rb
index d593cbb223a..d87fc1e4b86 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -95,8 +95,7 @@ class Project < ActiveRecord::Base
unless: :ci_cd_settings,
if: proc { ProjectCiCdSetting.available? }
- after_create :set_last_activity_at
- after_create :set_last_repository_updated_at
+ after_create :set_timestamps_for_create
after_update :update_forks_visibility_level
before_destroy :remove_private_deploy_keys
@@ -124,6 +123,7 @@ class Project < ActiveRecord::Base
alias_attribute :title, :name
# Relations
+ belongs_to :pool_repository
belongs_to :creator, class_name: 'User'
belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id'
belongs_to :namespace
@@ -135,6 +135,7 @@ class Project < ActiveRecord::Base
# Project services
has_one :campfire_service
+ has_one :discord_service
has_one :drone_ci_service
has_one :emails_on_push_service
has_one :pipelines_email_service
@@ -181,7 +182,7 @@ class Project < ActiveRecord::Base
has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
# Merge Requests for target project should be removed with it
- has_many :merge_requests, foreign_key: 'target_project_id'
+ has_many :merge_requests, foreign_key: 'target_project_id', inverse_of: :target_project
has_many :source_of_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest'
has_many :issues
has_many :labels, class_name: 'ProjectLabel'
@@ -254,7 +255,7 @@ class Project < ActiveRecord::Base
has_many :variables, class_name: 'Ci::Variable'
has_many :triggers, class_name: 'Ci::Trigger'
has_many :environments
- has_many :deployments
+ has_many :deployments, -> { success }
has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
has_many :project_deploy_tokens
has_many :deploy_tokens, through: :project_deploy_tokens
@@ -665,7 +666,7 @@ class Project < ActiveRecord::Base
remove_import_data
end
- # This method is overriden in EE::Project model
+ # This method is overridden in EE::Project model
def remove_import_data
import_data&.destroy
end
@@ -1829,7 +1830,7 @@ class Project < ActiveRecord::Base
end
def deployment_variables(environment: nil)
- deployment_platform(environment: environment)&.predefined_variables || []
+ deployment_platform(environment: environment)&.predefined_variables(project: self) || []
end
def auto_devops_variables
@@ -2073,6 +2074,10 @@ class Project < ActiveRecord::Base
storage_version != LATEST_STORAGE_VERSION
end
+ def snippets_visible?(user = nil)
+ Ability.allowed?(user, :read_project_snippet, self)
+ end
+
private
def use_hashed_storage
@@ -2102,13 +2107,8 @@ class Project < ActiveRecord::Base
gitlab_shell.exists?(repository_storage, "#{disk_path}.git")
end
- # set last_activity_at to the same as created_at
- def set_last_activity_at
- update_column(:last_activity_at, self.created_at)
- end
-
- def set_last_repository_updated_at
- update_column(:last_repository_updated_at, self.created_at)
+ def set_timestamps_for_create
+ update_columns(last_activity_at: self.created_at, last_repository_updated_at: self.created_at)
end
def cross_namespace_reference?(from)
diff --git a/app/models/project_import_state.rb b/app/models/project_import_state.rb
index d59cb43dea4..7126bb66d80 100644
--- a/app/models/project_import_state.rb
+++ b/app/models/project_import_state.rb
@@ -56,4 +56,17 @@ class ProjectImportState < ActiveRecord::Base
end
end
end
+
+ def mark_as_failed(error_message)
+ original_errors = errors.dup
+ sanitized_message = Gitlab::UrlSanitizer.sanitize(error_message)
+
+ fail_op
+
+ update_column(:last_error, sanitized_message)
+ rescue ActiveRecord::ActiveRecordError => e
+ Rails.logger.error("Error setting import status to failed: #{e.message}. Original error: #{sanitized_message}")
+ ensure
+ @errors = original_errors
+ end
end
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
index d121d088ff6..a252052200a 100644
--- a/app/models/project_services/bamboo_service.rb
+++ b/app/models/project_services/bamboo_service.rb
@@ -86,14 +86,16 @@ class BambooService < CiService
end
def read_build_page(response)
- if response.code != 200 || response.dig('results', 'results', 'size') == '0'
- # If actual build link can't be determined, send user to build summary page.
- URI.join("#{bamboo_url}/", "browse/#{build_key}").to_s
- else
- # If actual build link is available, go to build result page.
- result_key = response.dig('results', 'results', 'result', get_build_result_index, 'planResultKey', 'key')
- URI.join("#{bamboo_url}/", "browse/#{result_key}").to_s
- end
+ key =
+ if response.code != 200 || response.dig('results', 'results', 'size') == '0'
+ # If actual build link can't be determined, send user to build summary page.
+ build_key
+ else
+ # If actual build link is available, go to build result page.
+ response.dig('results', 'results', 'result', get_build_result_index, 'planResultKey', 'key')
+ end
+
+ build_url("browse/#{key}")
end
def read_commit_status(response)
@@ -117,7 +119,7 @@ class BambooService < CiService
end
def build_url(path)
- URI.join("#{bamboo_url}/", path).to_s
+ Gitlab::Utils.append_path(bamboo_url, path)
end
def get_path(path, query_params = {})
diff --git a/app/models/project_services/discord_service.rb b/app/models/project_services/discord_service.rb
new file mode 100644
index 00000000000..21afd14dbff
--- /dev/null
+++ b/app/models/project_services/discord_service.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require "discordrb/webhooks"
+
+class DiscordService < ChatNotificationService
+ def title
+ "Discord Notifications"
+ end
+
+ def description
+ "Receive event notifications in Discord"
+ end
+
+ def self.to_param
+ "discord"
+ end
+
+ def help
+ "This service sends notifications about project events to Discord channels.<br />
+ To set up this service:
+ <ol>
+ <li><a href='https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks'>Setup a custom Incoming Webhook</a>.</li>
+ <li>Paste the <strong>Webhook URL</strong> into the field below.</li>
+ <li>Select events below to enable notifications.</li>
+ </ol>"
+ end
+
+ def event_field(event)
+ # No-op.
+ end
+
+ def default_channel_placeholder
+ # No-op.
+ end
+
+ def default_fields
+ [
+ { type: "text", name: "webhook", placeholder: "e.g. https://discordapp.com/api/webhooks/…" },
+ { type: "checkbox", name: "notify_only_broken_pipelines" },
+ { type: "checkbox", name: "notify_only_default_branch" }
+ ]
+ end
+
+ private
+
+ def notify(message, opts)
+ client = Discordrb::Webhooks::Client.new(url: webhook)
+
+ client.execute do |builder|
+ builder.content = message.pretext
+ end
+ end
+
+ def custom_data(data)
+ super(data).merge(markdown: true)
+ end
+end
diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
index 158ae0bf255..5ccc2f019cb 100644
--- a/app/models/project_services/drone_ci_service.rb
+++ b/app/models/project_services/drone_ci_service.rb
@@ -39,11 +39,9 @@ class DroneCiService < CiService
end
def commit_status_path(sha, ref)
- url = [drone_url,
- "gitlab/#{project.full_path}/commits/#{sha}",
- "?branch=#{URI.encode(ref.to_s)}&access_token=#{token}"]
-
- URI.join(*url).to_s
+ Gitlab::Utils.append_path(
+ drone_url,
+ "gitlab/#{project.full_path}/commits/#{sha}?branch=#{URI.encode(ref.to_s)}&access_token=#{token}")
end
def commit_status(sha, ref)
@@ -74,11 +72,9 @@ class DroneCiService < CiService
end
def build_page(sha, ref)
- url = [drone_url,
- "gitlab/#{project.full_path}/redirect/commits/#{sha}",
- "?branch=#{URI.encode(ref.to_s)}"]
-
- URI.join(*url).to_s
+ Gitlab::Utils.append_path(
+ drone_url,
+ "gitlab/#{project.full_path}/redirect/commits/#{sha}?branch=#{URI.encode(ref.to_s)}")
end
def title
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index e1d342be188..f54497fc6d8 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -9,12 +9,12 @@ class IssueTrackerService < Service
# Override this method on services that uses different patterns
# This pattern does not support cross-project references
# The other code assumes that this pattern is a superset of all
- # overriden patterns. See ReferenceRegexes::EXTERNAL_PATTERN
+ # overridden patterns. See ReferenceRegexes.external_pattern
def self.reference_pattern(only_long: false)
if only_long
- /(\b[A-Z][A-Z0-9_]+-)(?<issue>\d+)/
+ /(\b[A-Z][A-Z0-9_]*-)(?<issue>\d+)/
else
- /(\b[A-Z][A-Z0-9_]+-|#{Issue.reference_prefix})(?<issue>\d+)/
+ /(\b[A-Z][A-Z0-9_]*-|#{Issue.reference_prefix})(?<issue>\d+)/
end
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 5a38f48c542..9066a0b7f1d 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -54,7 +54,7 @@ class JiraService < IssueTrackerService
{
username: self.username,
password: self.password,
- site: URI.join(url, '/').to_s,
+ site: URI.join(url, '/').to_s, # Intended to find the root
context_path: url.path.chomp('/'),
auth_type: :basic,
read_timeout: 120,
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index 798944d0c06..c52a531e5fe 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -104,7 +104,12 @@ class KubernetesService < DeploymentService
{ success: false, result: err }
end
- def predefined_variables
+ # Project param was added on
+ # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22011,
+ # as a way to keep this service compatible with
+ # Clusters::Platforms::Kubernetes, it won't be used on this method
+ # as it's only needed for Clusters::Cluster.
+ def predefined_variables(project:)
config = YAML.dump(kubeconfig)
Gitlab::Ci::Variables::Collection.new.tap do |variables|
@@ -198,9 +203,7 @@ class KubernetesService < DeploymentService
kubeclient = build_kube_client!
kubeclient.get_pods(namespace: actual_namespace).as_json
- rescue Kubeclient::HttpError => err
- raise err unless err.error_code == 404
-
+ rescue Kubeclient::ResourceNotFoundError
[]
end
diff --git a/app/models/project_services/mock_ci_service.rb b/app/models/project_services/mock_ci_service.rb
index 6883976f0c8..d8bba58dcbf 100644
--- a/app/models/project_services/mock_ci_service.rb
+++ b/app/models/project_services/mock_ci_service.rb
@@ -34,10 +34,9 @@ class MockCiService < CiService
# http://jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c
#
def build_page(sha, ref)
- url = [mock_service_url,
- "#{project.namespace.path}/#{project.path}/status/#{sha}"]
-
- URI.join(*url).to_s
+ Gitlab::Utils.append_path(
+ mock_service_url,
+ "#{project.namespace.path}/#{project.path}/status/#{sha}")
end
# Return string with build status or :error symbol
@@ -61,10 +60,9 @@ class MockCiService < CiService
end
def commit_status_path(sha)
- url = [mock_service_url,
- "#{project.namespace.path}/#{project.path}/status/#{sha}.json"]
-
- URI.join(*url).to_s
+ Gitlab::Utils.append_path(
+ mock_service_url,
+ "#{project.namespace.path}/#{project.path}/status/#{sha}.json")
end
def read_commit_status(response)
diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb
index eeeff5e802a..b8e17087db5 100644
--- a/app/models/project_services/teamcity_service.rb
+++ b/app/models/project_services/teamcity_service.rb
@@ -132,7 +132,7 @@ class TeamcityService < CiService
end
def build_url(path)
- URI.join("#{teamcity_url}/", path).to_s
+ Gitlab::Utils.append_path(teamcity_url, path)
end
def get_path(path)
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 37a1dd64052..6e179f61a7b 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -912,10 +912,6 @@ class Repository
async_remove_remote(remote_name) if tmp_remote_name
end
- def fetch_remote(remote, forced: false, ssh_auth: nil, no_tags: false, prune: true)
- gitlab_shell.fetch_remote(raw_repository, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags, prune: prune)
- end
-
def async_remove_remote(remote_name)
return unless remote_name
@@ -1018,6 +1014,18 @@ class Repository
message: merge_request.title)
end
+ def update_submodule(user, submodule, commit_sha, message:, branch:)
+ with_cache_hooks do
+ raw.update_submodule(
+ user: user,
+ submodule: submodule,
+ commit_sha: commit_sha,
+ branch: branch,
+ message: message
+ )
+ end
+ end
+
def blob_data_at(sha, path)
blob = blob_at(sha, path)
return unless blob
diff --git a/app/models/service.rb b/app/models/service.rb
index 4dbda7acab6..5b8bf6e7cf0 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -253,6 +253,7 @@ class Service < ActiveRecord::Base
bugzilla
campfire
custom_issue_tracker
+ discord
drone_ci
emails_on_push
external_wiki
diff --git a/app/models/shard.rb b/app/models/shard.rb
new file mode 100644
index 00000000000..2e75bc91df0
--- /dev/null
+++ b/app/models/shard.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class Shard < ActiveRecord::Base
+ # Store shard names from the configuration file in the database. This is not a
+ # list of active shards - we just want to assign an immutable, unique ID to
+ # every shard name for easy indexing / referencing.
+ def self.populate!
+ return unless table_exists?
+
+ # The GitLab config does not change for the lifecycle of the process
+ in_config = Gitlab.config.repositories.storages.keys.map(&:to_s)
+ in_db = all.pluck(:name)
+
+ # This may race with other processes creating shards at the same time, but
+ # `by_name` will handle that correctly
+ missing = in_config - in_db
+ missing.map { |name| by_name(name) }
+ end
+
+ def self.by_name(name)
+ find_or_create_by(name: name)
+ rescue ActiveRecord::RecordNotUnique
+ retry
+ end
+end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 1c5846b4023..11856b55902 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -63,6 +63,62 @@ class Snippet < ActiveRecord::Base
attr_spammable :title, spam_title: true
attr_spammable :content, spam_description: true
+ def self.with_optional_visibility(value = nil)
+ if value
+ where(visibility_level: value)
+ else
+ all
+ end
+ end
+
+ def self.only_global_snippets
+ where(project_id: nil)
+ end
+
+ def self.only_include_projects_visible_to(current_user = nil)
+ levels = Gitlab::VisibilityLevel.levels_for_user(current_user)
+
+ joins(:project).where('projects.visibility_level IN (?)', levels)
+ end
+
+ def self.only_include_projects_with_snippets_enabled(include_private: false)
+ column = ProjectFeature.access_level_attribute(:snippets)
+ levels = [ProjectFeature::ENABLED, ProjectFeature::PUBLIC]
+
+ levels << ProjectFeature::PRIVATE if include_private
+
+ joins(project: :project_feature)
+ .where(project_features: { column => levels })
+ end
+
+ def self.only_include_authorized_projects(current_user)
+ where(
+ 'EXISTS (?)',
+ ProjectAuthorization
+ .select(1)
+ .where('project_id = snippets.project_id')
+ .where(user_id: current_user.id)
+ )
+ end
+
+ def self.for_project_with_user(project, user = nil)
+ return none unless project.snippets_visible?(user)
+
+ if user && project.team.member?(user)
+ project.snippets
+ else
+ project.snippets.public_to_user(user)
+ end
+ end
+
+ def self.visible_to_or_authored_by(user)
+ where(
+ 'snippets.visibility_level IN (?) OR snippets.author_id = ?',
+ Gitlab::VisibilityLevel.levels_for_user(user),
+ user.id
+ )
+ end
+
def self.reference_prefix
'$'
end
@@ -81,27 +137,6 @@ class Snippet < ActiveRecord::Base
@link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/)
end
- # Returns a collection of snippets that are either public or visible to the
- # logged in user.
- #
- # This method does not verify the user actually has the access to the project
- # the snippet is in, so it should be only used on a relation that's already scoped
- # for project access
- def self.public_or_visible_to_user(user = nil)
- if user
- authorized = user
- .project_authorizations
- .select(1)
- .where('project_authorizations.project_id = snippets.project_id')
-
- levels = Gitlab::VisibilityLevel.levels_for_user(user)
-
- where('EXISTS (?) OR snippets.visibility_level IN (?) or snippets.author_id = (?)', authorized, levels, user.id)
- else
- public_to_user
- end
- end
-
def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{id}"
diff --git a/app/models/upload.rb b/app/models/upload.rb
index 23bc9ca42fc..e01e9c6a4f0 100644
--- a/app/models/upload.rb
+++ b/app/models/upload.rb
@@ -11,7 +11,8 @@ class Upload < ActiveRecord::Base
validates :model, presence: true
validates :uploader, presence: true
- scope :with_files_stored_locally, -> { where(store: [nil, ObjectStorage::Store::LOCAL]) }
+ scope :with_files_stored_locally, -> { where(store: ObjectStorage::Store::LOCAL) }
+ scope :with_files_stored_remotely, -> { where(store: ObjectStorage::Store::REMOTE) }
before_save :calculate_checksum!, if: :foreground_checksummable?
after_commit :schedule_checksum, if: :checksummable?
@@ -46,7 +47,18 @@ class Upload < ActiveRecord::Base
end
def exist?
- File.exist?(absolute_path)
+ exist = File.exist?(absolute_path)
+
+ # Help sysadmins find missing upload files
+ if persisted? && !exist
+ if Gitlab::Sentry.enabled?
+ Raven.capture_message("Upload file does not exist", extra: self.attributes)
+ end
+
+ Gitlab::Metrics.counter(:upload_file_does_not_exist_total, 'The number of times an upload record could not find its file').increment
+ end
+
+ exist
end
def uploader_context
@@ -57,8 +69,6 @@ class Upload < ActiveRecord::Base
end
def local?
- return true if store.nil?
-
store == ObjectStorage::Store::LOCAL
end
diff --git a/app/models/user.rb b/app/models/user.rb
index cc2cd1b7723..a400058e87e 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -88,7 +88,7 @@ class User < ActiveRecord::Base
has_one :namespace, -> { where(type: nil) }, dependent: :destroy, foreign_key: :owner_id, inverse_of: :owner, autosave: true # rubocop:disable Cop/ActiveRecordDependent
# Profile
- has_many :keys, -> { where(type: ['Key', nil]) }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :keys, -> { regular_keys }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
has_many :gpg_keys
@@ -347,7 +347,11 @@ class User < ActiveRecord::Base
# Find a User by their primary email or any associated secondary email
def find_by_any_email(email, confirmed: false)
- by_any_email(email, confirmed: confirmed).take
+ return unless email
+
+ downcased = email.downcase
+
+ find_by_private_commit_email(downcased) || by_any_email(downcased, confirmed: confirmed).take
end
# Returns a relation containing all the users for the given Email address
@@ -361,6 +365,12 @@ class User < ActiveRecord::Base
from_union([users, emails])
end
+ def find_by_private_commit_email(email)
+ user_id = Gitlab::PrivateCommitEmail.user_id_for_email(email)
+
+ find_by(id: user_id)
+ end
+
def filter(filter_name)
case filter_name
when 'admins'
@@ -460,12 +470,6 @@ class User < ActiveRecord::Base
by_username(username).take!
end
- def find_by_personal_access_token(token_string)
- return unless token_string
-
- PersonalAccessTokensFinder.new(state: 'active').find_by_token(token_string)&.user # rubocop: disable CodeReuse/Finder
- end
-
# Returns a user for the given SSH key.
def find_by_ssh_key_id(key_id)
Key.find_by(id: key_id)&.user
@@ -639,6 +643,10 @@ class User < ActiveRecord::Base
def commit_email
return self.email unless has_attribute?(:commit_email)
+ if super == Gitlab::PrivateCommitEmail::TOKEN
+ return private_commit_email
+ end
+
# The commit email is the same as the primary email if undefined
super.presence || self.email
end
@@ -651,6 +659,10 @@ class User < ActiveRecord::Base
has_attribute?(:commit_email) && super
end
+ def private_commit_email
+ Gitlab::PrivateCommitEmail.for_user(self)
+ end
+
# see if the new email is already a verified secondary email
def check_for_verified_email
skip_reconfirmation! if emails.confirmed.where(email: self.email).any?
@@ -941,12 +953,17 @@ class User < ActiveRecord::Base
if !Gitlab.config.ldap.enabled
false
elsif ldap_user?
- !last_credential_check_at || (last_credential_check_at + 1.hour) < Time.now
+ !last_credential_check_at || (last_credential_check_at + ldap_sync_time) < Time.now
else
false
end
end
+ def ldap_sync_time
+ # This number resides in this method so it can be redefined in EE.
+ 1.hour
+ end
+
def try_obtain_ldap_lease
# After obtaining this lease LDAP checks will be blocked for 600 seconds
# (10 minutes) for this user.
@@ -1021,13 +1038,21 @@ class User < ActiveRecord::Base
def verified_emails
verified_emails = []
verified_emails << email if primary_email_verified?
+ verified_emails << private_commit_email
verified_emails.concat(emails.confirmed.pluck(:email))
verified_emails
end
def verified_email?(check_email)
downcased = check_email.downcase
- email == downcased ? primary_email_verified? : emails.confirmed.where(email: downcased).exists?
+
+ if email == downcased
+ primary_email_verified?
+ else
+ user_id = Gitlab::PrivateCommitEmail.user_id_for_email(downcased)
+
+ user_id == id || emails.confirmed.where(email: downcased).exists?
+ end
end
def hook_attrs
diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb
index 6cd91abc261..32d0407800f 100644
--- a/app/models/user_preference.rb
+++ b/app/models/user_preference.rb
@@ -4,7 +4,7 @@ class UserPreference < ActiveRecord::Base
# We could use enums, but Rails 4 doesn't support multiple
# enum options with same name for multiple fields, also it creates
# extra methods that aren't really needed here.
- NOTES_FILTERS = { all_notes: 0, only_comments: 1 }.freeze
+ NOTES_FILTERS = { all_notes: 0, only_comments: 1, only_activity: 2 }.freeze
belongs_to :user
@@ -14,7 +14,8 @@ class UserPreference < ActiveRecord::Base
def notes_filters
{
s_('Notes|Show all activity') => NOTES_FILTERS[:all_notes],
- s_('Notes|Show comments only') => NOTES_FILTERS[:only_comments]
+ s_('Notes|Show comments only') => NOTES_FILTERS[:only_comments],
+ s_('Notes|Show history only') => NOTES_FILTERS[:only_activity]
}
end
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index c5e349ae913..7769c3d71c0 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -151,12 +151,6 @@ class WikiPage
last_version&.sha
end
- # Returns the Date that this latest version was
- # created on.
- def created_at
- @page.version.date
- end
-
# Returns boolean True or False if this instance
# is an old version of the page.
def historical?
@@ -195,7 +189,7 @@ class WikiPage
update_attributes(attrs)
save(page_details: title) do
- wiki.create_page(title, content, format, message)
+ wiki.create_page(title, content, format, attrs[:message])
end
end
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index 3858b29c82c..0ca3e696f46 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -20,12 +20,17 @@ module Ci
@subject.project.branch_allows_collaboration?(@user, @subject.ref)
end
+ condition(:archived, scope: :subject) do
+ @subject.archived?
+ end
+
condition(:terminal, scope: :subject) do
@subject.has_terminal?
end
- rule { protected_ref }.policy do
+ rule { protected_ref | archived }.policy do
prevent :update_build
+ prevent :update_commit_status
prevent :erase_build
end
diff --git a/app/policies/clusters/cluster_policy.rb b/app/policies/clusters/cluster_policy.rb
index 147943a3d6c..d6d590687e2 100644
--- a/app/policies/clusters/cluster_policy.rb
+++ b/app/policies/clusters/cluster_policy.rb
@@ -4,11 +4,7 @@ module Clusters
class ClusterPolicy < BasePolicy
alias_method :cluster, :subject
+ delegate { cluster.group }
delegate { cluster.project }
-
- rule { can?(:maintainer_access) }.policy do
- enable :update_cluster
- enable :admin_cluster
- end
end
end
diff --git a/app/policies/deployment_policy.rb b/app/policies/deployment_policy.rb
index 56ac898b6ab..d4f2f3c52b1 100644
--- a/app/policies/deployment_policy.rb
+++ b/app/policies/deployment_policy.rb
@@ -2,4 +2,13 @@
class DeploymentPolicy < BasePolicy
delegate { @subject.project }
+
+ condition(:can_retry_deployable) do
+ can?(:update_build, @subject.deployable)
+ end
+
+ rule { ~can_retry_deployable }.policy do
+ prevent :create_deployment
+ prevent :update_deployment
+ end
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 73c93b22c95..6b4e56ef5e4 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -65,6 +65,10 @@ class GroupPolicy < BasePolicy
enable :create_projects
enable :admin_pipeline
enable :admin_build
+ enable :read_cluster
+ enable :create_cluster
+ enable :update_cluster
+ enable :admin_cluster
end
rule { owner }.policy do
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index a76a083bceb..1c082945299 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -258,6 +258,8 @@ class ProjectPolicy < BasePolicy
enable :update_pages
enable :read_cluster
enable :create_cluster
+ enable :update_cluster
+ enable :admin_cluster
enable :create_environment_terminal
end
diff --git a/app/presenters/blob_presenter.rb b/app/presenters/blob_presenter.rb
new file mode 100644
index 00000000000..6323c1b3389
--- /dev/null
+++ b/app/presenters/blob_presenter.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class BlobPresenter < Gitlab::View::Presenter::Simple
+ presents :blob
+
+ def highlight(plain: nil)
+ blob.load_all_data! if blob.respond_to?(:load_all_data!)
+
+ Gitlab::Highlight.highlight(
+ blob.path,
+ blob.data,
+ language: blob.language_from_gitattributes,
+ plain: plain
+ )
+ end
+end
diff --git a/app/presenters/clusterable_presenter.rb b/app/presenters/clusterable_presenter.rb
new file mode 100644
index 00000000000..9cc137aa3bd
--- /dev/null
+++ b/app/presenters/clusterable_presenter.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+class ClusterablePresenter < Gitlab::View::Presenter::Delegated
+ presents :clusterable
+
+ def self.fabricate(clusterable, **attributes)
+ presenter_class = "#{clusterable.class.name}ClusterablePresenter".constantize
+ attributes_with_presenter_class = attributes.merge(presenter_class: presenter_class)
+
+ Gitlab::View::Presenter::Factory
+ .new(clusterable, attributes_with_presenter_class)
+ .fabricate!
+ end
+
+ def can_create_cluster?
+ can?(current_user, :create_cluster, clusterable)
+ end
+
+ def index_path
+ polymorphic_path([clusterable, :clusters])
+ end
+
+ def new_path
+ new_polymorphic_path([clusterable, :cluster])
+ end
+
+ def create_user_clusters_path
+ polymorphic_path([clusterable, :clusters], action: :create_user)
+ end
+
+ def create_gcp_clusters_path
+ polymorphic_path([clusterable, :clusters], action: :create_gcp)
+ end
+
+ def cluster_status_cluster_path(cluster, params = {})
+ raise NotImplementedError
+ end
+
+ def install_applications_cluster_path(cluster, application)
+ raise NotImplementedError
+ end
+
+ def cluster_path(cluster, params = {})
+ raise NotImplementedError
+ end
+
+ def empty_state_help_text
+ nil
+ end
+
+ def sidebar_text
+ raise NotImplementedError
+ end
+
+ def learn_more_link
+ raise NotImplementedError
+ end
+end
diff --git a/app/presenters/clusters/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb
index dfdd8e82f97..7e6eccb648c 100644
--- a/app/presenters/clusters/cluster_presenter.rb
+++ b/app/presenters/clusters/cluster_presenter.rb
@@ -11,5 +11,15 @@ module Clusters
def can_toggle_cluster?
can?(current_user, :update_cluster, cluster) && created?
end
+
+ def show_path
+ if cluster.project_type?
+ project_cluster_path(project, cluster)
+ elsif cluster.group_type?
+ group_cluster_path(group, cluster)
+ else
+ raise NotImplementedError
+ end
+ end
end
end
diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb
index a866e76df5a..0cd77da6303 100644
--- a/app/presenters/commit_status_presenter.rb
+++ b/app/presenters/commit_status_presenter.rb
@@ -10,7 +10,8 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
missing_dependency_failure: 'There has been a missing dependency failure',
runner_unsupported: 'Your runner is outdated, please upgrade your runner',
stale_schedule: 'Delayed job could not be executed by some reason, please try again',
- job_execution_timeout: 'The script exceeded the maximum execution time set for the job'
+ job_execution_timeout: 'The script exceeded the maximum execution time set for the job',
+ archived_failure: 'The job is archived and cannot be run'
}.freeze
private_constant :CALLOUT_FAILURE_MESSAGES
@@ -30,6 +31,6 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
end
def unrecoverable?
- script_failure? || missing_dependency_failure?
+ script_failure? || missing_dependency_failure? || archived_failure?
end
end
diff --git a/app/presenters/group_clusterable_presenter.rb b/app/presenters/group_clusterable_presenter.rb
new file mode 100644
index 00000000000..d963c188559
--- /dev/null
+++ b/app/presenters/group_clusterable_presenter.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+class GroupClusterablePresenter < ClusterablePresenter
+ extend ::Gitlab::Utils::Override
+ include ActionView::Helpers::UrlHelper
+
+ override :cluster_status_cluster_path
+ def cluster_status_cluster_path(cluster, params = {})
+ cluster_status_group_cluster_path(clusterable, cluster, params)
+ end
+
+ override :install_applications_cluster_path
+ def install_applications_cluster_path(cluster, application)
+ install_applications_group_cluster_path(clusterable, cluster, application)
+ end
+
+ override :cluster_path
+ def cluster_path(cluster, params = {})
+ group_cluster_path(clusterable, cluster, params)
+ end
+
+ override :empty_state_help_text
+ def empty_state_help_text
+ s_('ClusterIntegration|Adding an integration to your group will share the cluster across all your projects.')
+ end
+
+ override :sidebar_text
+ def sidebar_text
+ s_('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.')
+ end
+
+ override :learn_more_link
+ def learn_more_link
+ link_to(s_('ClusterIntegration|Learn more about group Kubernetes clusters'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
+ end
+end
diff --git a/app/presenters/project_clusterable_presenter.rb b/app/presenters/project_clusterable_presenter.rb
new file mode 100644
index 00000000000..63e69b91b11
--- /dev/null
+++ b/app/presenters/project_clusterable_presenter.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class ProjectClusterablePresenter < ClusterablePresenter
+ extend ::Gitlab::Utils::Override
+ include ActionView::Helpers::UrlHelper
+
+ override :cluster_status_cluster_path
+ def cluster_status_cluster_path(cluster, params = {})
+ cluster_status_project_cluster_path(clusterable, cluster, params)
+ end
+
+ override :install_applications_cluster_path
+ def install_applications_cluster_path(cluster, application)
+ install_applications_project_cluster_path(clusterable, cluster, application)
+ end
+
+ override :cluster_path
+ def cluster_path(cluster, params = {})
+ project_cluster_path(clusterable, cluster, params)
+ end
+
+ override :sidebar_text
+ def sidebar_text
+ s_('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.')
+ end
+
+ override :learn_more_link
+ def learn_more_link
+ link_to(s_('ClusterIntegration|Learn more about Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
+ end
+end
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index 79cd3606aec..d61124fa787 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -176,7 +176,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
AnchorData.new(false,
_('New file'),
project_new_blob_path(project, default_branch || 'master'),
- 'new')
+ 'success')
end
end
diff --git a/app/serializers/build_action_entity.rb b/app/serializers/build_action_entity.rb
index 0db7875aa87..95833c3528f 100644
--- a/app/serializers/build_action_entity.rb
+++ b/app/serializers/build_action_entity.rb
@@ -12,7 +12,8 @@ class BuildActionEntity < Grape::Entity
end
expose :playable?, as: :playable
- expose :scheduled_at, if: -> (build) { build.scheduled? }
+ expose :scheduled?, as: :scheduled
+ expose :scheduled_at, if: -> (*) { scheduled? }
expose :unschedule_path, if: -> (build) { build.scheduled? } do |build|
unschedule_project_job_path(build.project, build)
@@ -25,4 +26,8 @@ class BuildActionEntity < Grape::Entity
def playable?
build.playable? && can?(request.current_user, :update_build, build)
end
+
+ def scheduled?
+ build.scheduled?
+ end
end
diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb
index 344148a1fb7..aa1d9e6292c 100644
--- a/app/serializers/deployment_entity.rb
+++ b/app/serializers/deployment_entity.rb
@@ -25,4 +25,5 @@ class DeploymentEntity < Grape::Entity
expose :commit, using: CommitEntity
expose :deployable, using: JobEntity
expose :manual_actions, using: JobEntity
+ expose :scheduled_actions, using: JobEntity
end
diff --git a/app/serializers/environment_status_entity.rb b/app/serializers/environment_status_entity.rb
index f87cc894d2f..f6321b9e520 100644
--- a/app/serializers/environment_status_entity.rb
+++ b/app/serializers/environment_status_entity.rb
@@ -11,7 +11,7 @@ class EnvironmentStatusEntity < Grape::Entity
project_environment_path(es.project, es.environment)
end
- expose :metrics_url, if: ->(*) { can_read_environment? && environment.has_metrics? } do |es|
+ expose :metrics_url, if: ->(*) { can_read_environment? && deployment.has_metrics? } do |es|
metrics_project_environment_deployment_path(es.project, es.environment, es.deployment)
end
@@ -37,7 +37,7 @@ class EnvironmentStatusEntity < Grape::Entity
es.deployment.try(:formatted_deployment_time)
end
- expose :changes, if: ->(*) { Feature.enabled?(:ci_environments_status_changes, project) }
+ expose :changes
private
@@ -45,6 +45,10 @@ class EnvironmentStatusEntity < Grape::Entity
object.environment
end
+ def deployment
+ object.deployment
+ end
+
def project
object.environment.project
end
diff --git a/app/serializers/job_entity.rb b/app/serializers/job_entity.rb
index 0b19cb16955..d0099ae77f2 100644
--- a/app/serializers/job_entity.rb
+++ b/app/serializers/job_entity.rb
@@ -7,9 +7,10 @@ class JobEntity < Grape::Entity
expose :name
expose :started?, as: :started
+ expose :archived?, as: :archived
expose :build_path do |build|
- build.target_url || path_to(:namespace_project_job, build)
+ build_path(build)
end
expose :retry_path, if: -> (*) { retryable? } do |build|
@@ -17,7 +18,11 @@ class JobEntity < Grape::Entity
end
expose :cancel_path, if: -> (*) { cancelable? } do |build|
- path_to(:cancel_namespace_project_job, build)
+ path_to(
+ :cancel_namespace_project_job,
+ build,
+ { continue: { to: build_path(build) } }
+ )
end
expose :play_path, if: -> (*) { playable? } do |build|
@@ -29,6 +34,7 @@ class JobEntity < Grape::Entity
end
expose :playable?, as: :playable
+ expose :scheduled?, as: :scheduled
expose :scheduled_at, if: -> (*) { scheduled? }
expose :created_at
expose :updated_at
@@ -60,8 +66,12 @@ class JobEntity < Grape::Entity
build.detailed_status(request.current_user)
end
- def path_to(route, build)
- send("#{route}_path", build.project.namespace, build.project, build) # rubocop:disable GitlabSecurity/PublicSend
+ def path_to(route, build, params = {})
+ send("#{route}_path", build.project.namespace, build.project, build, params) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ def build_path(build)
+ build.target_url || path_to(:namespace_project_job, build)
end
def failed?
diff --git a/app/serializers/user_preference_entity.rb b/app/serializers/user_preference_entity.rb
index fbdaab459b3..b99f80424db 100644
--- a/app/serializers/user_preference_entity.rb
+++ b/app/serializers/user_preference_entity.rb
@@ -7,4 +7,8 @@ class UserPreferenceEntity < Grape::Entity
expose :notes_filters do |user_preference|
UserPreference.notes_filters
end
+
+ expose :default_notes_filter do |user_preference|
+ UserPreference::NOTES_FILTERS[:all_notes]
+ end
end
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index 893b37b831a..f764536e762 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -99,7 +99,7 @@ module Auth
##
# Because we do not have two way communication with registry yet,
# we create a container repository image resource when push to the
- # registry is successfuly authorized.
+ # registry is successfully authorized.
#
def ensure_container_repository!(path, actions)
return if path.has_repository?
diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb
index 7dd87034410..43a26f4264e 100644
--- a/app/services/boards/issues/move_service.rb
+++ b/app/services/boards/issues/move_service.rb
@@ -70,10 +70,8 @@ module Boards
label_ids =
if moving_to_list.movable?
moving_from_list.label_id
- elsif board.group_board?
- ::Label.on_group_boards(parent.id).pluck(:label_id)
else
- ::Label.on_project_boards(parent.id).pluck(:label_id)
+ ::Label.on_board(board.id).pluck(:label_id)
end
Array(label_ids).compact
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index 5a7be921389..e06f1c05843 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -82,6 +82,11 @@ module Ci
return false
end
+ if build.archived?
+ build.drop!(:archived_failure)
+ return false
+ end
+
build.run!
true
end
diff --git a/app/services/clusters/applications/check_installation_progress_service.rb b/app/services/clusters/applications/check_installation_progress_service.rb
index 5017fa093f3..ca0f7b30053 100644
--- a/app/services/clusters/applications/check_installation_progress_service.rb
+++ b/app/services/clusters/applications/check_installation_progress_service.rb
@@ -14,8 +14,10 @@ module Clusters
else
check_timeout
end
- rescue Kubeclient::HttpError
- app.make_errored!("Kubernetes error") unless app.errored?
+ rescue Kubeclient::HttpError => e
+ Rails.logger.error("Kubernetes error: #{e.error_code} #{e.message}")
+ Gitlab::Sentry.track_acceptable_exception(e, extra: { scope: 'kubernetes', app_id: app.id })
+ app.make_errored!("Kubernetes error: #{e.error_code}") unless app.errored?
end
private
@@ -51,7 +53,8 @@ module Clusters
def remove_installation_pod
helm_api.delete_pod!(install_command.pod_name)
- rescue
+ rescue => e
+ Rails.logger.error("Kubernetes error: #{e.class.name} #{e.message}")
# no-op
end
diff --git a/app/services/clusters/applications/create_service.rb b/app/services/clusters/applications/create_service.rb
index 55f917798de..844807c2581 100644
--- a/app/services/clusters/applications/create_service.rb
+++ b/app/services/clusters/applications/create_service.rb
@@ -42,10 +42,20 @@ module Clusters
def builders
{
"helm" => -> (cluster) { cluster.application_helm || cluster.build_application_helm },
- "ingress" => -> (cluster) { cluster.application_ingress || cluster.build_application_ingress },
+ "ingress" => -> (cluster) { cluster.application_ingress || cluster.build_application_ingress }
+ }.tap do |hash|
+ hash.merge!(project_builders) if cluster.project_type?
+ end
+ end
+
+ # These applications will need extra configuration to enable them to work
+ # with groups of projects
+ def project_builders
+ {
"prometheus" => -> (cluster) { cluster.application_prometheus || cluster.build_application_prometheus },
"runner" => -> (cluster) { cluster.application_runner || cluster.build_application_runner },
- "jupyter" => -> (cluster) { cluster.application_jupyter || cluster.build_application_jupyter }
+ "jupyter" => -> (cluster) { cluster.application_jupyter || cluster.build_application_jupyter },
+ "knative" => -> (cluster) { cluster.application_knative || cluster.build_application_knative }
}
end
diff --git a/app/services/clusters/applications/install_service.rb b/app/services/clusters/applications/install_service.rb
index dd8d2ed5eb6..f4385748c43 100644
--- a/app/services/clusters/applications/install_service.rb
+++ b/app/services/clusters/applications/install_service.rb
@@ -12,9 +12,13 @@ module Clusters
ClusterWaitForAppInstallationWorker.perform_in(
ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id)
- rescue Kubeclient::HttpError
- app.make_errored!("Kubernetes error.")
- rescue StandardError
+ rescue Kubeclient::HttpError => e
+ Rails.logger.error("Kubernetes error: #{e.error_code} #{e.message}")
+ Gitlab::Sentry.track_acceptable_exception(e, extra: { scope: 'kubernetes', app_id: app.id })
+ app.make_errored!("Kubernetes error: #{e.error_code}")
+ rescue StandardError => e
+ Rails.logger.error "Can't start installation process: #{e.class.name} #{e.message}"
+ Gitlab::Sentry.track_acceptable_exception(e, extra: { scope: 'kubernetes', app_id: app.id })
app.make_errored!("Can't start installation process.")
end
end
diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb
index cd843b8ffa8..5a9da053780 100644
--- a/app/services/clusters/create_service.rb
+++ b/app/services/clusters/create_service.rb
@@ -8,10 +8,11 @@ module Clusters
@current_user, @params = user, params.dup
end
- def execute(project:, access_token: nil)
- raise ArgumentError, _('Instance does not support multiple Kubernetes clusters') unless can_create_cluster?(project)
+ def execute(access_token: nil)
+ raise ArgumentError, 'Unknown clusterable provided' unless clusterable
+ raise ArgumentError, _('Instance does not support multiple Kubernetes clusters') unless can_create_cluster?
- cluster_params = params.merge(user: current_user, cluster_type: :project_type, projects: [project])
+ cluster_params = params.merge(user: current_user).merge(clusterable_params)
cluster_params[:provider_gcp_attributes].try do |provider|
provider[:access_token] = access_token
end
@@ -27,9 +28,24 @@ module Clusters
Clusters::Cluster.create(cluster_params)
end
+ def clusterable
+ @clusterable ||= params.delete(:clusterable)
+ end
+
+ def clusterable_params
+ case clusterable
+ when ::Project
+ { cluster_type: :project_type, projects: [clusterable] }
+ when ::Group
+ { cluster_type: :group_type, groups: [clusterable] }
+ else
+ raise NotImplementedError
+ end
+ end
+
# EE would override this method
- def can_create_cluster?(project)
- project.clusters.empty?
+ def can_create_cluster?
+ clusterable.clusters.empty?
end
end
end
diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb
index 6ee63db8eb9..3df43657fa0 100644
--- a/app/services/clusters/gcp/finalize_creation_service.rb
+++ b/app/services/clusters/gcp/finalize_creation_service.rb
@@ -11,8 +11,9 @@ module Clusters
configure_provider
create_gitlab_service_account!
configure_kubernetes
-
cluster.save!
+ configure_project_service_account
+
rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
provider.make_errored!("Failed to request to CloudPlatform; #{e.message}")
rescue Kubeclient::HttpError => e
@@ -24,7 +25,10 @@ module Clusters
private
def create_gitlab_service_account!
- Clusters::Gcp::Kubernetes::CreateServiceAccountService.new(kube_client, rbac: create_rbac_cluster?).execute
+ Clusters::Gcp::Kubernetes::CreateServiceAccountService.gitlab_creator(
+ kube_client,
+ rbac: create_rbac_cluster?
+ ).execute
end
def configure_provider
@@ -44,7 +48,20 @@ module Clusters
end
def request_kubernetes_token
- Clusters::Gcp::Kubernetes::FetchKubernetesTokenService.new(kube_client).execute
+ Clusters::Gcp::Kubernetes::FetchKubernetesTokenService.new(
+ kube_client,
+ Clusters::Gcp::Kubernetes::GITLAB_ADMIN_TOKEN_NAME,
+ Clusters::Gcp::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAMESPACE
+ ).execute
+ end
+
+ def configure_project_service_account
+ kubernetes_namespace = cluster.find_or_initialize_kubernetes_namespace(cluster.cluster_project)
+
+ Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService.new(
+ cluster: cluster,
+ kubernetes_namespace: kubernetes_namespace
+ ).execute
end
def authorization_type
diff --git a/app/services/clusters/gcp/kubernetes.rb b/app/services/clusters/gcp/kubernetes.rb
index d014d73b3e8..90ed529670c 100644
--- a/app/services/clusters/gcp/kubernetes.rb
+++ b/app/services/clusters/gcp/kubernetes.rb
@@ -3,11 +3,12 @@
module Clusters
module Gcp
module Kubernetes
- SERVICE_ACCOUNT_NAME = 'gitlab'
- SERVICE_ACCOUNT_NAMESPACE = 'default'
- SERVICE_ACCOUNT_TOKEN_NAME = 'gitlab-token'
- CLUSTER_ROLE_BINDING_NAME = 'gitlab-admin'
- CLUSTER_ROLE_NAME = 'cluster-admin'
+ GITLAB_SERVICE_ACCOUNT_NAME = 'gitlab'
+ GITLAB_SERVICE_ACCOUNT_NAMESPACE = 'default'
+ GITLAB_ADMIN_TOKEN_NAME = 'gitlab-token'
+ GITLAB_CLUSTER_ROLE_BINDING_NAME = 'gitlab-admin'
+ GITLAB_CLUSTER_ROLE_NAME = 'cluster-admin'
+ PROJECT_CLUSTER_ROLE_NAME = 'edit'
end
end
end
diff --git a/app/services/clusters/gcp/kubernetes/create_or_update_namespace_service.rb b/app/services/clusters/gcp/kubernetes/create_or_update_namespace_service.rb
new file mode 100644
index 00000000000..2b607681082
--- /dev/null
+++ b/app/services/clusters/gcp/kubernetes/create_or_update_namespace_service.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Gcp
+ module Kubernetes
+ class CreateOrUpdateNamespaceService
+ def initialize(cluster:, kubernetes_namespace:)
+ @cluster = cluster
+ @kubernetes_namespace = kubernetes_namespace
+ @platform = cluster.platform
+ end
+
+ def execute
+ configure_kubernetes_namespace
+ create_project_service_account
+ configure_kubernetes_token
+
+ kubernetes_namespace.save!
+ end
+
+ private
+
+ attr_reader :cluster, :kubernetes_namespace, :platform
+
+ def configure_kubernetes_namespace
+ kubernetes_namespace.configure_predefined_credentials
+ end
+
+ def create_project_service_account
+ Clusters::Gcp::Kubernetes::CreateServiceAccountService.namespace_creator(
+ platform.kubeclient,
+ service_account_name: kubernetes_namespace.service_account_name,
+ service_account_namespace: kubernetes_namespace.namespace,
+ rbac: platform.rbac?
+ ).execute
+ end
+
+ def configure_kubernetes_token
+ kubernetes_namespace.service_account_token = fetch_service_account_token
+ end
+
+ def fetch_service_account_token
+ Clusters::Gcp::Kubernetes::FetchKubernetesTokenService.new(
+ platform.kubeclient,
+ kubernetes_namespace.token_name,
+ kubernetes_namespace.namespace
+ ).execute
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/gcp/kubernetes/create_service_account_service.rb b/app/services/clusters/gcp/kubernetes/create_service_account_service.rb
index d17744591e6..dfc4bf7a358 100644
--- a/app/services/clusters/gcp/kubernetes/create_service_account_service.rb
+++ b/app/services/clusters/gcp/kubernetes/create_service_account_service.rb
@@ -4,46 +4,96 @@ module Clusters
module Gcp
module Kubernetes
class CreateServiceAccountService
- attr_reader :kubeclient, :rbac
-
- def initialize(kubeclient, rbac:)
+ def initialize(kubeclient, service_account_name:, service_account_namespace:, token_name:, rbac:, namespace_creator: false, role_binding_name: nil)
@kubeclient = kubeclient
+ @service_account_name = service_account_name
+ @service_account_namespace = service_account_namespace
+ @token_name = token_name
@rbac = rbac
+ @namespace_creator = namespace_creator
+ @role_binding_name = role_binding_name
+ end
+
+ def self.gitlab_creator(kubeclient, rbac:)
+ self.new(
+ kubeclient,
+ service_account_name: Clusters::Gcp::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAME,
+ service_account_namespace: Clusters::Gcp::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAMESPACE,
+ token_name: Clusters::Gcp::Kubernetes::GITLAB_ADMIN_TOKEN_NAME,
+ rbac: rbac
+ )
+ end
+
+ def self.namespace_creator(kubeclient, service_account_name:, service_account_namespace:, rbac:)
+ self.new(
+ kubeclient,
+ service_account_name: service_account_name,
+ service_account_namespace: service_account_namespace,
+ token_name: "#{service_account_namespace}-token",
+ rbac: rbac,
+ namespace_creator: true,
+ role_binding_name: "gitlab-#{service_account_namespace}"
+ )
end
def execute
+ ensure_project_namespace_exists if namespace_creator
kubeclient.create_service_account(service_account_resource)
kubeclient.create_secret(service_account_token_resource)
- kubeclient.create_cluster_role_binding(cluster_role_binding_resource) if rbac
+ create_role_or_cluster_role_binding if rbac
end
private
+ attr_reader :kubeclient, :service_account_name, :service_account_namespace, :token_name, :rbac, :namespace_creator, :role_binding_name
+
+ def ensure_project_namespace_exists
+ Gitlab::Kubernetes::Namespace.new(
+ service_account_namespace,
+ kubeclient
+ ).ensure_exists!
+ end
+
+ def create_role_or_cluster_role_binding
+ if namespace_creator
+ kubeclient.create_role_binding(role_binding_resource)
+ else
+ kubeclient.create_cluster_role_binding(cluster_role_binding_resource)
+ end
+ end
+
def service_account_resource
- Gitlab::Kubernetes::ServiceAccount.new(service_account_name, service_account_namespace).generate
+ Gitlab::Kubernetes::ServiceAccount.new(
+ service_account_name,
+ service_account_namespace
+ ).generate
end
def service_account_token_resource
Gitlab::Kubernetes::ServiceAccountToken.new(
- SERVICE_ACCOUNT_TOKEN_NAME, service_account_name, service_account_namespace).generate
+ token_name,
+ service_account_name,
+ service_account_namespace
+ ).generate
end
def cluster_role_binding_resource
subjects = [{ kind: 'ServiceAccount', name: service_account_name, namespace: service_account_namespace }]
Gitlab::Kubernetes::ClusterRoleBinding.new(
- CLUSTER_ROLE_BINDING_NAME,
- CLUSTER_ROLE_NAME,
+ Clusters::Gcp::Kubernetes::GITLAB_CLUSTER_ROLE_BINDING_NAME,
+ Clusters::Gcp::Kubernetes::GITLAB_CLUSTER_ROLE_NAME,
subjects
).generate
end
- def service_account_name
- SERVICE_ACCOUNT_NAME
- end
-
- def service_account_namespace
- SERVICE_ACCOUNT_NAMESPACE
+ def role_binding_resource
+ Gitlab::Kubernetes::RoleBinding.new(
+ name: role_binding_name,
+ role_name: Clusters::Gcp::Kubernetes::PROJECT_CLUSTER_ROLE_NAME,
+ namespace: service_account_namespace,
+ service_account_name: service_account_name
+ ).generate
end
end
end
diff --git a/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb b/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb
index 9e09345c8dc..4ad04ab801e 100644
--- a/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb
+++ b/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb
@@ -4,10 +4,12 @@ module Clusters
module Gcp
module Kubernetes
class FetchKubernetesTokenService
- attr_reader :kubeclient
+ attr_reader :kubeclient, :service_account_token_name, :namespace
- def initialize(kubeclient)
+ def initialize(kubeclient, service_account_token_name, namespace)
@kubeclient = kubeclient
+ @service_account_token_name = service_account_token_name
+ @namespace = namespace
end
def execute
@@ -18,11 +20,8 @@ module Clusters
private
def get_secret
- kubeclient.get_secret(SERVICE_ACCOUNT_TOKEN_NAME, SERVICE_ACCOUNT_NAMESPACE).as_json
- rescue Kubeclient::HttpError => err
- raise err unless err.error_code == 404
-
- nil
+ kubeclient.get_secret(service_account_token_name, namespace).as_json
+ rescue Kubeclient::ResourceNotFoundError
end
end
end
diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb
index 2fbd442fc2e..fbf71f02837 100644
--- a/app/services/commits/change_service.rb
+++ b/app/services/commits/change_service.rb
@@ -24,8 +24,12 @@ module Commits
start_project: @start_project,
start_branch_name: @start_branch)
rescue Gitlab::Git::Repository::CreateTreeError
- error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically.
- This #{@commit.change_type_title(current_user)} may already have been #{action.to_s.dasherize}ed, or a more recent commit may have updated some of its content."
+ act = action.to_s.dasherize
+ type = @commit.change_type_title(current_user)
+
+ error_msg = "Sorry, we cannot #{act} this #{type} automatically. " \
+ "This #{type} may already have been #{act}ed, or a more recent " \
+ "commit may have updated some of its content."
raise ChangeError, error_msg
end
end
diff --git a/app/services/commits/commit_patch_service.rb b/app/services/commits/commit_patch_service.rb
new file mode 100644
index 00000000000..9253cfaac20
--- /dev/null
+++ b/app/services/commits/commit_patch_service.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module Commits
+ class CommitPatchService < CreateService
+ # Requires:
+ # - project: `Project` to be committed into
+ # - user: `User` that will be the committer
+ # - params:
+ # - branch_name: `String` the branch that will be committed into
+ # - start_branch: `String` the branch that will will started from
+ # - patches: `Gitlab::Git::Patches::Collection` that contains the patches
+ def initialize(*args)
+ super
+
+ @patches = Gitlab::Git::Patches::Collection.new(Array(params[:patches]))
+ end
+
+ private
+
+ def new_branch?
+ !repository.branch_exists?(@branch_name)
+ end
+
+ def create_commit!
+ if @start_branch && new_branch?
+ prepare_branch!
+ end
+
+ Gitlab::Git::Patches::CommitPatches
+ .new(current_user, project.repository, @branch_name, @patches)
+ .commit
+ end
+
+ def prepare_branch!
+ branch_result = CreateBranchService.new(project, current_user)
+ .execute(@branch_name, @start_branch)
+
+ if branch_result[:status] != :success
+ raise ChangeError, branch_result[:message]
+ end
+ end
+
+ # Overridden from the Commits::CreateService, to skip some validations we
+ # don't need:
+ # - validate_on_branch!
+ # Not needed, the patches are applied on top of HEAD if the branch did not
+ # exist
+ # - validate_branch_existence!
+ # Not needed because we continue applying patches on the branch if it
+ # already existed, and create it if it did not exist.
+ def validate!
+ validate_patches!
+ validate_new_branch_name! if new_branch?
+ validate_permissions!
+ end
+
+ def validate_patches!
+ raise_error("Patches are too big") unless @patches.valid_size?
+ end
+ end
+end
diff --git a/app/services/commits/create_service.rb b/app/services/commits/create_service.rb
index 3ce9acc833c..34593e12bd5 100644
--- a/app/services/commits/create_service.rb
+++ b/app/services/commits/create_service.rb
@@ -19,7 +19,12 @@ module Commits
new_commit = create_commit!
success(result: new_commit)
- rescue ValidationError, ChangeError, Gitlab::Git::Index::IndexError, Gitlab::Git::CommitError, Gitlab::Git::PreReceiveError => ex
+ rescue ValidationError,
+ ChangeError,
+ Gitlab::Git::Index::IndexError,
+ Gitlab::Git::CommitError,
+ Gitlab::Git::PreReceiveError,
+ Gitlab::Git::CommandError => ex
error(ex.message)
end
diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb
deleted file mode 100644
index bb3f605da28..00000000000
--- a/app/services/create_deployment_service.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-# frozen_string_literal: true
-
-class CreateDeploymentService
- attr_reader :job
-
- delegate :expanded_environment_name,
- :variables,
- :project,
- to: :job
-
- def initialize(job)
- @job = job
- end
-
- def execute
- return unless executable?
-
- ActiveRecord::Base.transaction do
- environment.external_url = expanded_environment_url if
- expanded_environment_url
-
- environment.fire_state_event(action)
-
- break unless environment.save
- break if environment.stopped?
-
- deploy.tap(&:update_merge_request_metrics!)
- end
- end
-
- private
-
- def executable?
- project && job.environment.present? && environment
- end
-
- def deploy
- project.deployments.create(
- environment: environment,
- ref: job.ref,
- tag: job.tag,
- sha: job.sha,
- user: job.user,
- deployable: job,
- on_stop: on_stop)
- end
-
- def environment
- @environment ||= job.persisted_environment
- end
-
- def environment_options
- @environment_options ||= job.options&.dig(:environment) || {}
- end
-
- def expanded_environment_url
- return @expanded_environment_url if defined?(@expanded_environment_url)
-
- @expanded_environment_url =
- ExpandVariables.expand(environment_url, variables) if environment_url
- end
-
- def environment_url
- environment_options[:url]
- end
-
- def on_stop
- environment_options[:on_stop]
- end
-
- def action
- environment_options[:action] || 'start'
- end
-end
diff --git a/app/services/issuable/clone/attributes_rewriter.rb b/app/services/issuable/clone/attributes_rewriter.rb
new file mode 100644
index 00000000000..0300cc0d8d3
--- /dev/null
+++ b/app/services/issuable/clone/attributes_rewriter.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+module Issuable
+ module Clone
+ class AttributesRewriter < ::Issuable::Clone::BaseService
+ def initialize(current_user, original_entity, new_entity)
+ @current_user = current_user
+ @original_entity = original_entity
+ @new_entity = new_entity
+ end
+
+ def execute
+ new_entity.update(milestone: cloneable_milestone, labels: cloneable_labels)
+ copy_resource_label_events
+ end
+
+ private
+
+ def cloneable_milestone
+ title = original_entity.milestone&.title
+ return unless title
+
+ params = { title: title, project_ids: new_entity.project&.id, group_ids: group&.id }
+
+ milestones = MilestonesFinder.new(params).execute
+ milestones.first
+ end
+
+ def cloneable_labels
+ params = {
+ project_id: new_entity.project&.id,
+ group_id: group&.id,
+ title: original_entity.labels.select(:title),
+ include_ancestor_groups: true
+ }
+
+ params[:only_group_labels] = true if new_parent.is_a?(Group)
+
+ LabelsFinder.new(current_user, params).execute
+ end
+
+ def copy_resource_label_events
+ original_entity.resource_label_events.find_in_batches do |batch|
+ events = batch.map do |event|
+ entity_key = new_entity.is_a?(Issue) ? 'issue_id' : 'epic_id'
+ # rubocop: disable CodeReuse/ActiveRecord
+ event.attributes
+ .except('id', 'reference', 'reference_html')
+ .merge(entity_key => new_entity.id, 'action' => ResourceLabelEvent.actions[event.action])
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+
+ Gitlab::Database.bulk_insert(ResourceLabelEvent.table_name, events)
+ end
+ end
+
+ def entity_key
+ new_entity.class.name.parameterize('_').foreign_key
+ end
+ end
+ end
+end
diff --git a/app/services/issuable/clone/base_service.rb b/app/services/issuable/clone/base_service.rb
new file mode 100644
index 00000000000..42dd9c666f5
--- /dev/null
+++ b/app/services/issuable/clone/base_service.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module Issuable
+ module Clone
+ class BaseService < IssuableBaseService
+ attr_reader :original_entity, :new_entity
+
+ alias_method :old_project, :project
+
+ def execute(original_entity, new_project = nil)
+ @original_entity = original_entity
+
+ # Using transaction because of a high resources footprint
+ # on rewriting notes (unfolding references)
+ #
+ ActiveRecord::Base.transaction do
+ @new_entity = create_new_entity
+
+ update_new_entity
+ update_old_entity
+ create_notes
+ end
+ end
+
+ private
+
+ def update_new_entity
+ rewriters = [ContentRewriter, AttributesRewriter]
+
+ rewriters.each do |rewriter|
+ rewriter.new(current_user, original_entity, new_entity).execute
+ end
+ end
+
+ def update_old_entity
+ close_issue
+ end
+
+ def create_notes
+ add_note_from
+ add_note_to
+ end
+
+ def close_issue
+ close_service = Issues::CloseService.new(old_project, current_user)
+ close_service.execute(original_entity, notifications: false, system_note: false)
+ end
+
+ def new_parent
+ new_entity.project ? new_entity.project : new_entity.group
+ end
+
+ def group
+ if new_entity.project&.group && current_user.can?(:read_group, new_entity.project.group)
+ new_entity.project.group
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/issuable/clone/content_rewriter.rb b/app/services/issuable/clone/content_rewriter.rb
new file mode 100644
index 00000000000..e1e0b75085d
--- /dev/null
+++ b/app/services/issuable/clone/content_rewriter.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+module Issuable
+ module Clone
+ class ContentRewriter < ::Issuable::Clone::BaseService
+ def initialize(current_user, original_entity, new_entity)
+ @current_user = current_user
+ @original_entity = original_entity
+ @new_entity = new_entity
+ @project = original_entity.project
+ end
+
+ def execute
+ rewrite_description
+ rewrite_award_emoji(original_entity, new_entity)
+ rewrite_notes
+ end
+
+ private
+
+ def rewrite_description
+ new_entity.update(description: rewrite_content(original_entity.description))
+ end
+
+ def rewrite_notes
+ original_entity.notes_with_associations.find_each do |note|
+ new_note = note.dup
+ new_params = {
+ project: new_entity.project, noteable: new_entity,
+ note: rewrite_content(new_note.note),
+ created_at: note.created_at,
+ updated_at: note.updated_at
+ }
+
+ if note.system_note_metadata
+ new_params[:system_note_metadata] = note.system_note_metadata.dup
+ end
+
+ new_note.update(new_params)
+
+ rewrite_award_emoji(note, new_note)
+ end
+ end
+
+ def rewrite_content(content)
+ return unless content
+
+ rewriters = [Gitlab::Gfm::ReferenceRewriter, Gitlab::Gfm::UploadsRewriter]
+
+ rewriters.inject(content) do |text, klass|
+ rewriter = klass.new(text, old_project, current_user)
+ rewriter.rewrite(new_parent)
+ end
+ end
+
+ def rewrite_award_emoji(old_awardable, new_awardable)
+ old_awardable.award_emoji.each do |award|
+ new_award = award.dup
+ new_award.awardable = new_awardable
+ new_award.save
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 3e8b9f84042..e32e262ac31 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -3,6 +3,14 @@
class IssuableBaseService < BaseService
private
+ attr_accessor :params, :skip_milestone_email
+
+ def initialize(project, user = nil, params = {})
+ super
+
+ @skip_milestone_email = @params.delete(:skip_milestone_email)
+ end
+
def filter_params(issuable)
ability_name = :"admin_#{issuable.to_ability_name}"
@@ -118,12 +126,12 @@ class IssuableBaseService < BaseService
merge_quick_actions_into_params!(issuable)
end
- def merge_quick_actions_into_params!(issuable)
+ def merge_quick_actions_into_params!(issuable, only: nil)
original_description = params.fetch(:description, issuable.description)
description, command_params =
QuickActions::InterpretService.new(project, current_user)
- .execute(original_description, issuable)
+ .execute(original_description, issuable, only: only)
# Avoid a description already set on an issuable to be overwritten by a nil
params[:description] = description if description
diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb
index d2bdba1e627..41b6a96b005 100644
--- a/app/services/issues/move_service.rb
+++ b/app/services/issues/move_service.rb
@@ -1,165 +1,66 @@
# frozen_string_literal: true
module Issues
- class MoveService < Issues::BaseService
+ class MoveService < Issuable::Clone::BaseService
MoveError = Class.new(StandardError)
- def execute(issue, new_project)
- @old_issue = issue
- @old_project = @project
- @new_project = new_project
+ def execute(issue, target_project)
+ @target_project = target_project
- unless issue.can_move?(current_user, new_project)
+ unless issue.can_move?(current_user, @target_project)
raise MoveError, 'Cannot move issue due to insufficient permissions!'
end
- if @project == new_project
+ if @project == @target_project
raise MoveError, 'Cannot move issue to project it originates from!'
end
- # Using transaction because of a high resources footprint
- # on rewriting notes (unfolding references)
- #
- ActiveRecord::Base.transaction do
- @new_issue = create_new_issue
-
- update_new_issue
- update_old_issue
- end
+ super
notify_participants
- @new_issue
+ new_entity
end
private
- def update_new_issue
- rewrite_notes
- copy_resource_label_events
- rewrite_issue_award_emoji
- add_note_moved_from
- end
+ def update_old_entity
+ super
- def update_old_issue
- add_note_moved_to
- close_issue
mark_as_moved
end
- def create_new_issue
- new_params = { id: nil, iid: nil, label_ids: cloneable_label_ids,
- milestone_id: cloneable_milestone_id,
- project: @new_project, author: @old_issue.author,
- description: rewrite_content(@old_issue.description),
- assignee_ids: @old_issue.assignee_ids }
-
- new_params = @old_issue.serializable_hash.symbolize_keys.merge(new_params)
- CreateService.new(@new_project, @current_user, new_params).execute
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def cloneable_label_ids
- params = {
- project_id: @new_project.id,
- title: @old_issue.labels.pluck(:title),
- include_ancestor_groups: true
- }
+ def create_new_entity
+ new_params = {
+ id: nil,
+ iid: nil,
+ project: @target_project,
+ author: original_entity.author,
+ assignee_ids: original_entity.assignee_ids
+ }
- LabelsFinder.new(current_user, params).execute.pluck(:id)
+ new_params = original_entity.serializable_hash.symbolize_keys.merge(new_params)
+ CreateService.new(@target_project, @current_user, new_params).execute
end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def cloneable_milestone_id
- title = @old_issue.milestone&.title
- return unless title
-
- if @new_project.group && can?(current_user, :read_group, @new_project.group)
- group_id = @new_project.group.id
- end
-
- params =
- { title: title, project_ids: @new_project.id, group_ids: group_id }
- milestones = MilestonesFinder.new(params).execute
- milestones.first&.id
- end
-
- def rewrite_notes
- @old_issue.notes_with_associations.find_each do |note|
- new_note = note.dup
- new_params = { project: @new_project, noteable: @new_issue,
- note: rewrite_content(new_note.note),
- created_at: note.created_at,
- updated_at: note.updated_at }
-
- new_note.update(new_params)
-
- rewrite_award_emoji(note, new_note)
- end
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def copy_resource_label_events
- @old_issue.resource_label_events.find_in_batches do |batch|
- events = batch.map do |event|
- event.attributes
- .except('id', 'reference', 'reference_html')
- .merge('issue_id' => @new_issue.id, 'action' => ResourceLabelEvent.actions[event.action])
- end
-
- Gitlab::Database.bulk_insert(ResourceLabelEvent.table_name, events)
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def rewrite_issue_award_emoji
- rewrite_award_emoji(@old_issue, @new_issue)
- end
-
- def rewrite_award_emoji(old_awardable, new_awardable)
- old_awardable.award_emoji.each do |award|
- new_award = award.dup
- new_award.awardable = new_awardable
- new_award.save
- end
- end
-
- def rewrite_content(content)
- return unless content
-
- rewriters = [Gitlab::Gfm::ReferenceRewriter,
- Gitlab::Gfm::UploadsRewriter]
-
- rewriters.inject(content) do |text, klass|
- rewriter = klass.new(text, @old_project, @current_user)
- rewriter.rewrite(@new_project)
- end
+ def mark_as_moved
+ original_entity.update(moved_to: new_entity)
end
- def close_issue
- close_service = CloseService.new(@old_project, @current_user)
- close_service.execute(@old_issue, notifications: false, system_note: false)
+ def notify_participants
+ notification_service.async.issue_moved(original_entity, new_entity, @current_user)
end
- def add_note_moved_from
- SystemNoteService.noteable_moved(@new_issue, @new_project,
- @old_issue, @current_user,
+ def add_note_from
+ SystemNoteService.noteable_moved(new_entity, @target_project,
+ original_entity, current_user,
direction: :from)
end
- def add_note_moved_to
- SystemNoteService.noteable_moved(@old_issue, @old_project,
- @new_issue, @current_user,
+ def add_note_to
+ SystemNoteService.noteable_moved(original_entity, old_project,
+ new_entity, current_user,
direction: :to)
end
-
- def mark_as_moved
- @old_issue.update(moved_to: @new_issue)
- end
-
- def notify_participants
- notification_service.async.issue_moved(@old_issue, @new_issue, @current_user)
- end
end
end
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index b54b0bf6ef6..fba252b0bae 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -48,6 +48,8 @@ module Issues
notification_service.async.relabeled_issue(issue, added_labels, current_user)
end
+ handle_milestone_change(issue)
+
added_mentions = issue.mentioned_users - old_mentioned_users
if added_mentions.present?
@@ -91,6 +93,18 @@ module Issues
private
+ def handle_milestone_change(issue)
+ return if skip_milestone_email
+
+ return unless issue.previous_changes.include?('milestone_id')
+
+ if issue.milestone.nil?
+ notification_service.async.removed_milestone_issue(issue, current_user)
+ else
+ notification_service.async.changed_milestone_issue(issue, issue.milestone, current_user)
+ end
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def get_issue_if_allowed(id, board_group_id = nil)
return unless id
diff --git a/app/services/keys/destroy_service.rb b/app/services/keys/destroy_service.rb
index e2ae4047941..159455f80f3 100644
--- a/app/services/keys/destroy_service.rb
+++ b/app/services/keys/destroy_service.rb
@@ -6,7 +6,7 @@ module Keys
key.destroy if destroy_possible?(key)
end
- # overriden in EE::Keys::DestroyService
+ # overridden in EE::Keys::DestroyService
def destroy_possible?(key)
true
end
diff --git a/app/services/members/base_service.rb b/app/services/members/base_service.rb
index 8248f1441d7..d734571f835 100644
--- a/app/services/members/base_service.rb
+++ b/app/services/members/base_service.rb
@@ -10,7 +10,7 @@ module Members
end
def after_execute(args)
- # overriden in EE::Members modules
+ # overridden in EE::Members modules
end
private
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 0e76d2cc3ab..6c69452e2ab 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -6,8 +6,12 @@ module MergeRequests
def execute
@params_issue_iid = params.delete(:issue_iid)
+ self.merge_request = MergeRequest.new
+ # TODO: this should handle all quick actions that don't have side effects
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/53658
+ merge_quick_actions_into_params!(merge_request, only: [:target_branch])
+ merge_request.assign_attributes(params)
- self.merge_request = MergeRequest.new(params)
merge_request.author = current_user
merge_request.compare_commits = []
merge_request.source_project = find_source_project
diff --git a/app/services/merge_requests/get_urls_service.rb b/app/services/merge_requests/get_urls_service.rb
index 35a22449e34..7c88c9abb41 100644
--- a/app/services/merge_requests/get_urls_service.rb
+++ b/app/services/merge_requests/get_urls_service.rb
@@ -50,8 +50,8 @@ module MergeRequests
end
def url_for_new_merge_request(branch_name)
- url = Gitlab::Routing.url_helpers.project_new_merge_request_url(project, branch_name)
-
+ merge_request_params = { source_branch: branch_name }
+ url = Gitlab::Routing.url_helpers.project_new_merge_request_url(project, merge_request: merge_request_params)
{ branch_name: branch_name, url: url, new_merge_request: true }
end
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index f01872b205e..5fe48da1cd6 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -2,18 +2,18 @@
module MergeRequests
class RefreshService < MergeRequests::BaseService
+ attr_reader :push
+
def execute(oldrev, newrev, ref)
- push = Gitlab::Git::Push.new(@project, oldrev, newrev, ref)
- return true unless push.branch_push?
+ @push = Gitlab::Git::Push.new(@project, oldrev, newrev, ref)
+ return true unless @push.branch_push?
- refresh_merge_requests!(push)
+ refresh_merge_requests!
end
private
- def refresh_merge_requests!(push)
- @push = push
-
+ def refresh_merge_requests!
Gitlab::GitalyClient.allow_n_plus_1_calls(&method(:find_new_commits))
# Be sure to close outstanding MRs before reloading them to avoid generating an
# empty diff during a manual merge
@@ -87,11 +87,8 @@ module MergeRequests
filter_merge_requests(merge_requests).each do |merge_request|
if branch_and_project_match?(merge_request) || @push.force_push?
merge_request.reload_diff(current_user)
- else
- mr_commit_ids = merge_request.commit_shas
- push_commit_ids = @commits.map(&:id)
- matches = mr_commit_ids & push_commit_ids
- merge_request.reload_diff(current_user) if matches.any?
+ elsif merge_request.includes_any_commits?(push_commit_ids)
+ merge_request.reload_diff(current_user)
end
merge_request.mark_as_unchecked
@@ -104,6 +101,10 @@ module MergeRequests
end
# rubocop: enable CodeReuse/ActiveRecord
+ def push_commit_ids
+ @push_commit_ids ||= @commits.map(&:id)
+ end
+
def branch_and_project_match?(merge_request)
merge_request.source_project == @project &&
merge_request.source_branch == @push.branch_name
diff --git a/app/services/merge_requests/reload_diffs_service.rb b/app/services/merge_requests/reload_diffs_service.rb
index b4d48fe92ad..c64b2e99b52 100644
--- a/app/services/merge_requests/reload_diffs_service.rb
+++ b/app/services/merge_requests/reload_diffs_service.rb
@@ -29,14 +29,13 @@ module MergeRequests
# rubocop: disable CodeReuse/ActiveRecord
def clear_cache(new_diff)
- # Executing the iteration we cache highlighted diffs for each diff file of
- # MergeRequestDiff.
- cacheable_collection(new_diff).write_cache
-
# Remove cache for all diffs on this MR. Do not use the association on the
# model, as that will interfere with other actions happening when
# reloading the diff.
- MergeRequestDiff.where(merge_request: merge_request).each do |merge_request_diff|
+ MergeRequestDiff
+ .where(merge_request: merge_request)
+ .preload(merge_request: :target_project)
+ .find_each do |merge_request_diff|
next if merge_request_diff == new_diff
cacheable_collection(merge_request_diff).clear_cache
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index b112edbce7f..aacaf10d09c 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -58,6 +58,8 @@ module MergeRequests
merge_request.mark_as_unchecked
end
+ handle_milestone_change(merge_request)
+
added_labels = merge_request.labels - old_labels
if added_labels.present?
notification_service.async.relabeled_merge_request(
@@ -105,6 +107,18 @@ module MergeRequests
private
+ def handle_milestone_change(merge_request)
+ return if skip_milestone_email
+
+ return unless merge_request.previous_changes.include?('milestone_id')
+
+ if merge_request.milestone.nil?
+ notification_service.async.removed_milestone_merge_request(merge_request, current_user)
+ else
+ notification_service.async.changed_milestone_merge_request(merge_request, merge_request.milestone, current_user)
+ end
+ end
+
def create_branch_change_note(issuable, branch_type, old_branch, new_branch)
SystemNoteService.change_branch(
issuable, issuable.project, current_user, branch_type,
diff --git a/app/services/milestones/destroy_service.rb b/app/services/milestones/destroy_service.rb
index 7cda802c120..87c7a282081 100644
--- a/app/services/milestones/destroy_service.rb
+++ b/app/services/milestones/destroy_service.rb
@@ -4,7 +4,7 @@ module Milestones
class DestroyService < Milestones::BaseService
def execute(milestone)
Milestone.transaction do
- update_params = { milestone: nil }
+ update_params = { milestone: nil, skip_milestone_email: true }
milestone.issues.each do |issue|
Issues::UpdateService.new(parent, current_user, update_params).execute(issue)
diff --git a/app/services/notes/base_service.rb b/app/services/notes/base_service.rb
new file mode 100644
index 00000000000..c1260837c12
--- /dev/null
+++ b/app/services/notes/base_service.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Notes
+ class BaseService < ::BaseService
+ def clear_noteable_diffs_cache(note)
+ if note.is_a?(DiffNote) &&
+ note.discussion_first_note? &&
+ note.position.unfolded_diff?(project.repository)
+ note.noteable.diffs.clear_cache
+ end
+ end
+ end
+end
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index 049e6c5a871..e03789e3ca9 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module Notes
- class CreateService < ::BaseService
+ class CreateService < ::Notes::BaseService
def execute
merge_request_diff_head_sha = params.delete(:merge_request_diff_head_sha)
@@ -35,6 +35,7 @@ module Notes
if !only_commands && note.save
todo_service.new_note(note, current_user)
+ clear_noteable_diffs_cache(note)
end
if command_params.present?
diff --git a/app/services/notes/destroy_service.rb b/app/services/notes/destroy_service.rb
index 64e9accd97f..fa0c2c5c86b 100644
--- a/app/services/notes/destroy_service.rb
+++ b/app/services/notes/destroy_service.rb
@@ -1,11 +1,13 @@
# frozen_string_literal: true
module Notes
- class DestroyService < BaseService
+ class DestroyService < ::Notes::BaseService
def execute(note)
TodoService.new.destroy_target(note) do |note|
note.destroy
end
+
+ clear_noteable_diffs_cache(note)
end
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 50fa373025b..fb9c18ea75d 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -129,6 +129,14 @@ class NotificationService
relabeled_resource_email(issue, added_labels, current_user, :relabeled_issue_email)
end
+ def removed_milestone_issue(issue, current_user)
+ removed_milestone_resource_email(issue, current_user, :removed_milestone_issue_email)
+ end
+
+ def changed_milestone_issue(issue, new_milestone, current_user)
+ changed_milestone_resource_email(issue, new_milestone, current_user, :changed_milestone_issue_email)
+ end
+
# When create a merge request we should send an email to:
#
# * mr author
@@ -138,7 +146,6 @@ class NotificationService
# * users with custom level checked with "new merge request"
#
# In EE, approvers of the merge request are also included
- #
def new_merge_request(merge_request, current_user)
new_resource_email(merge_request, :new_merge_request_email)
end
@@ -208,6 +215,14 @@ class NotificationService
relabeled_resource_email(merge_request, added_labels, current_user, :relabeled_merge_request_email)
end
+ def removed_milestone_merge_request(merge_request, current_user)
+ removed_milestone_resource_email(merge_request, current_user, :removed_milestone_merge_request_email)
+ end
+
+ def changed_milestone_merge_request(merge_request, new_milestone, current_user)
+ changed_milestone_resource_email(merge_request, new_milestone, current_user, :changed_milestone_merge_request_email)
+ end
+
def close_mr(merge_request, current_user)
close_resource_email(merge_request, current_user, :closed_merge_request_email)
end
@@ -500,6 +515,30 @@ class NotificationService
end
end
+ def removed_milestone_resource_email(target, current_user, method)
+ recipients = NotificationRecipientService.build_recipients(
+ target,
+ current_user,
+ action: 'removed_milestone'
+ )
+
+ recipients.each do |recipient|
+ mailer.send(method, recipient.user.id, target.id, current_user.id).deliver_later
+ end
+ end
+
+ def changed_milestone_resource_email(target, milestone, current_user, method)
+ recipients = NotificationRecipientService.build_recipients(
+ target,
+ current_user,
+ action: 'changed_milestone'
+ )
+
+ recipients.each do |recipient|
+ mailer.send(method, recipient.user.id, target.id, milestone, current_user.id).deliver_later
+ end
+ end
+
def reopen_resource_email(target, current_user, method, status)
recipients = NotificationRecipientService.build_recipients(target, current_user, action: "reopen")
diff --git a/app/services/projects/move_project_authorizations_service.rb b/app/services/projects/move_project_authorizations_service.rb
index 2060a263751..2985ba89014 100644
--- a/app/services/projects/move_project_authorizations_service.rb
+++ b/app/services/projects/move_project_authorizations_service.rb
@@ -3,7 +3,7 @@
# NOTE: This service cannot be used directly because it is part of a
# a bigger process. Instead, use the service MoveAccessService which moves
# project memberships, project group links, authorizations and refreshes
-# the authorizations if neccessary
+# the authorizations if necessary
module Projects
class MoveProjectAuthorizationsService < BaseMoveRelationsService
def execute(source_project, remove_remaining_elements: true)
diff --git a/app/services/projects/move_project_group_links_service.rb b/app/services/projects/move_project_group_links_service.rb
index fb395ecb9a1..36afcd0c503 100644
--- a/app/services/projects/move_project_group_links_service.rb
+++ b/app/services/projects/move_project_group_links_service.rb
@@ -3,7 +3,7 @@
# NOTE: This service cannot be used directly because it is part of a
# a bigger process. Instead, use the service MoveAccessService which moves
# project memberships, project group links, authorizations and refreshes
-# the authorizations if neccessary
+# the authorizations if necessary
module Projects
class MoveProjectGroupLinksService < BaseMoveRelationsService
def execute(source_project, remove_remaining_elements: true)
diff --git a/app/services/projects/move_project_members_service.rb b/app/services/projects/move_project_members_service.rb
index f28f44adc03..faf389241d2 100644
--- a/app/services/projects/move_project_members_service.rb
+++ b/app/services/projects/move_project_members_service.rb
@@ -3,7 +3,7 @@
# NOTE: This service cannot be used directly because it is part of a
# a bigger process. Instead, use the service MoveAccessService which moves
# project memberships, project group links, authorizations and refreshes
-# the authorizations if neccessary
+# the authorizations if necessary
module Projects
class MoveProjectMembersService < BaseMoveRelationsService
def execute(source_project, remove_remaining_elements: true)
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index 751aae2696d..9c81de7e90e 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -23,13 +23,13 @@ module QuickActions
# Takes a text and interprets the commands that are extracted from it.
# Returns the content without commands, and hash of changes to be applied to a record.
- def execute(content, issuable)
+ def execute(content, issuable, only: nil)
return [content, {}] unless current_user.can?(:use_quick_actions)
@issuable = issuable
@updates = {}
- content, commands = extractor.extract_commands(content)
+ content, commands = extractor.extract_commands(content, only: only)
extract_updates(commands)
[content, @updates]
@@ -433,14 +433,14 @@ module QuickActions
end
end
- desc 'Add or substract spent time'
+ desc 'Add or subtract spent time'
explanation do |time_spent, time_spent_date|
if time_spent
if time_spent > 0
verb = 'Adds'
value = time_spent
else
- verb = 'Substracts'
+ verb = 'Subtracts'
value = -time_spent
end
diff --git a/app/services/search/group_service.rb b/app/services/search/group_service.rb
index 00372887985..34803d005e3 100644
--- a/app/services/search/group_service.rb
+++ b/app/services/search/group_service.rb
@@ -11,13 +11,11 @@ module Search
@group = group
end
- # rubocop: disable CodeReuse/ActiveRecord
def projects
return Project.none unless group
return @projects if defined? @projects
@projects = super.inside_path(group.full_path)
end
- # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/services/submodules/update_service.rb b/app/services/submodules/update_service.rb
new file mode 100644
index 00000000000..a6011a920bd
--- /dev/null
+++ b/app/services/submodules/update_service.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Submodules
+ class UpdateService < Commits::CreateService
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(*args)
+ super
+
+ @start_branch = @branch_name
+ @commit_sha = params[:commit_sha].presence
+ @submodule = params[:submodule].presence
+ @commit_message = params[:commit_message].presence || "Update submodule #{@submodule} with oid #{@commit_sha}"
+ end
+
+ def validate!
+ super
+
+ raise ValidationError, 'The repository is empty' if repository.empty?
+ end
+
+ def execute
+ super
+ rescue StandardError => e
+ error(e.message)
+ end
+
+ def create_commit!
+ repository.update_submodule(current_user,
+ @submodule,
+ @commit_sha,
+ message: @commit_message,
+ branch: @branch_name)
+ rescue ArgumentError, TypeError
+ raise ValidationError, 'Invalid parameters'
+ end
+ end
+end
diff --git a/app/services/update_deployment_service.rb b/app/services/update_deployment_service.rb
new file mode 100644
index 00000000000..aa7fcca1e2a
--- /dev/null
+++ b/app/services/update_deployment_service.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+class UpdateDeploymentService
+ attr_reader :deployment
+ attr_reader :deployable
+
+ delegate :environment, to: :deployment
+ delegate :variables, to: :deployable
+
+ def initialize(deployment)
+ @deployment = deployment
+ @deployable = deployment.deployable
+ end
+
+ def execute
+ deployment.create_ref
+ deployment.invalidate_cache
+
+ ActiveRecord::Base.transaction do
+ environment.external_url = expanded_environment_url if
+ expanded_environment_url
+
+ environment.fire_state_event(action)
+
+ break unless environment.save
+ break if environment.stopped?
+
+ deployment.tap(&:update_merge_request_metrics!)
+ end
+ end
+
+ private
+
+ def environment_options
+ @environment_options ||= deployable.options&.dig(:environment) || {}
+ end
+
+ def expanded_environment_url
+ return @expanded_environment_url if defined?(@expanded_environment_url)
+ return unless environment_url
+
+ @expanded_environment_url =
+ ExpandVariables.expand(environment_url, variables)
+ end
+
+ def environment_url
+ environment_options[:url]
+ end
+
+ def action
+ environment_options[:action] || 'start'
+ end
+end
diff --git a/app/services/users/build_service.rb b/app/services/users/build_service.rb
index de6ff92d1da..24ac20fdd29 100644
--- a/app/services/users/build_service.rb
+++ b/app/services/users/build_service.rb
@@ -95,10 +95,6 @@ module Users
if params[:reset_password]
user_params.merge!(force_random_password: true, password_expires_at: nil)
end
-
- if user_default_internal_regex_enabled? && !user_params.key?(:external)
- user_params[:external] = user_external?
- end
else
allowed_signup_params = signup_params
allowed_signup_params << :skip_confirmation if skip_authorization
@@ -109,6 +105,10 @@ module Users
end
end
+ if user_default_internal_regex_enabled? && !user_params.key?(:external)
+ user_params[:external] = user_external?
+ end
+
user_params
end
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index ffc1e5f75ca..e90599f2505 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -149,9 +149,9 @@ class FileUploader < GitlabUploader
# return a new uploader with a file copy on another project
def self.copy_to(uploader, to_project)
- moved = uploader.dup.tap do |u|
- u.model = to_project
- end
+ moved = self.new(to_project)
+ moved.object_store = uploader.object_store
+ moved.filename = uploader.filename
moved.copy_file(uploader.file)
moved
diff --git a/app/views/abuse_reports/new.html.haml b/app/views/abuse_reports/new.html.haml
index 391115a67b5..84c3dfd8b91 100644
--- a/app/views/abuse_reports/new.html.haml
+++ b/app/views/abuse_reports/new.html.haml
@@ -1,6 +1,10 @@
-- page_title "Report abuse"
-%h3.page-title Report abuse
-%p Please use this form to report users who create spam issues, comments or behave inappropriately.
+- page_title _("Report abuse to GitLab")
+%h3.page-title
+ = _('Report abuse to GitLab')
+%p
+ = _('Please use this form to report users to GitLab who create spam issues, comments or behave inappropriately.')
+%p
+ = _("A member of GitLab's abuse team will review your report as soon as possible.")
%hr
= form_for @abuse_report, html: { class: 'js-quick-submit js-requires-input'} do |f|
= form_errors(@abuse_report)
@@ -16,7 +20,7 @@
.col-sm-10
= f.text_area :message, class: "form-control", rows: 2, required: true, value: sanitize(@ref_url)
.form-text.text-muted
- Explain the problem with this user. If appropriate, provide a link to the relevant issue or comment.
+ = _('Explain the problem. If appropriate, provide a link to the relevant issue or comment.')
.form-actions
= f.submit "Send report", class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml
index 97be658cd34..0d42094fc89 100644
--- a/app/views/admin/application_settings/_ci_cd.html.haml
+++ b/app/views/admin/application_settings/_ci_cd.html.haml
@@ -41,5 +41,13 @@
The default unit is in seconds, but you can define an alternative. For example:
<code>4 mins 2 sec</code>, <code>2h42min</code>.
= link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration')
+ .form-group
+ = f.label :archive_builds_in_human_readable, 'Archive jobs', class: 'label-bold'
+ = f.text_field :archive_builds_in_human_readable, class: 'form-control', placeholder: 'never'
+ .form-text.text-muted
+ Set the duration for which the jobs will be considered as old and expired.
+ Once that time passes, the jobs will be archived and no longer able to be
+ retried. Make it empty to never expire jobs. It has to be no less than 1 day,
+ for example: <code>15 days</code>, <code>1 month</code>, <code>2 years</code>.
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_email.html.haml b/app/views/admin/application_settings/_email.html.haml
index 86339e61215..60a6be731ea 100644
--- a/app/views/admin/application_settings/_email.html.haml
+++ b/app/views/admin/application_settings/_email.html.haml
@@ -20,5 +20,11 @@
By default GitLab sends emails in HTML and plain text formats so mail
clients can choose what format to use. Disable this option if you only
want to send emails in plain text format.
+ .form-group
+ = f.label :commit_email_hostname, _('Custom hostname (for private commit emails)'), class: 'label-bold'
+ = f.text_field :commit_email_hostname, class: 'form-control'
+ .form-text.text-muted
+ - commit_email_hostname_docs_link = link_to _('Learn more'), help_page_path('user/admin_area/settings/email', anchor: 'custom-private-commit-email-hostname'), target: '_blank'
+ = _("This setting will update the hostname that is used to generate private commit emails. %{learn_more}").html_safe % { learn_more: commit_email_hostname_docs_link }
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml
index faa5854bb40..9aa705d9fa6 100644
--- a/app/views/admin/background_jobs/show.html.haml
+++ b/app/views/admin/background_jobs/show.html.haml
@@ -34,10 +34,13 @@
.clearfix
%p
%i.fa.fa-exclamation-circle
- If '[#{@concurrency} of #{@concurrency} busy]' is shown, restart GitLab with 'sudo service gitlab reload'.
+ If '[#{@concurrency} of #{@concurrency} busy]' is shown, restart GitLab.
+ = link_to sprite_icon('question', size: 16), help_page_path('administration/restart_gitlab')
+
%p
%i.fa.fa-exclamation-circle
If more than one sidekiq process is listed, stop GitLab, kill the remaining sidekiq processes (sudo pkill -u #{gitlab_config.user} -f sidekiq) and restart GitLab.
+ = link_to sprite_icon('question', size: 16), help_page_path('administration/restart_gitlab')
diff --git a/app/views/projects/clusters/_advanced_settings.html.haml b/app/views/clusters/clusters/_advanced_settings.html.haml
index 243e8cd9ba0..7037c80aa6b 100644
--- a/app/views/projects/clusters/_advanced_settings.html.haml
+++ b/app/views/clusters/clusters/_advanced_settings.html.haml
@@ -12,4 +12,4 @@
= s_('ClusterIntegration|Remove Kubernetes cluster integration')
%p
= s_("ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster.")
- = link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: s_("ClusterIntegration|Are you sure you want to remove this Kubernetes cluster's integration? This will not delete your actual Kubernetes cluster.")})
+ = link_to(s_('ClusterIntegration|Remove integration'), clusterable.cluster_path(@cluster), method: :delete, class: 'btn btn-danger', data: { confirm: s_("ClusterIntegration|Are you sure you want to remove this Kubernetes cluster's integration? This will not delete your actual Kubernetes cluster.")})
diff --git a/app/views/projects/clusters/_banner.html.haml b/app/views/clusters/clusters/_banner.html.haml
index 73cfea0ef92..160c5f009a7 100644
--- a/app/views/projects/clusters/_banner.html.haml
+++ b/app/views/clusters/clusters/_banner.html.haml
@@ -7,9 +7,3 @@
.hidden.js-cluster-success.bs-callout.bs-callout-success{ role: 'alert' }
= s_("ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine. Refresh the page to see Kubernetes cluster's details")
-
-- if show_cluster_security_warning?
- .js-cluster-security-warning.alert.alert-block.alert-dismissable.bs-callout.bs-callout-warning
- %button.close{ type: "button", data: { feature_id: UserCalloutsHelper::CLUSTER_SECURITY_WARNING, dismiss_endpoint: user_callouts_path } } &times;
- = s_("ClusterIntegration|The default cluster configuration grants access to many functionalities needed to successfully build and deploy a containerised application.")
- = link_to s_("More information"), help_page_path('user/project/clusters/index.md', anchor: 'security-implications')
diff --git a/app/views/clusters/clusters/_buttons.html.haml b/app/views/clusters/clusters/_buttons.html.haml
new file mode 100644
index 00000000000..db2e247e341
--- /dev/null
+++ b/app/views/clusters/clusters/_buttons.html.haml
@@ -0,0 +1,4 @@
+-# This partial is overridden in EE
+.nav-controls
+ %span.btn.btn-add-cluster.disabled.js-add-cluster
+ = s_("ClusterIntegration|Add Kubernetes cluster")
diff --git a/app/views/clusters/clusters/_cluster.html.haml b/app/views/clusters/clusters/_cluster.html.haml
new file mode 100644
index 00000000000..adeca013749
--- /dev/null
+++ b/app/views/clusters/clusters/_cluster.html.haml
@@ -0,0 +1,16 @@
+.card
+ .card-body.gl-responsive-table-row
+ .table-section.section-60
+ .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Kubernetes cluster")
+ .table-mobile-content
+ = link_to cluster.name, cluster.show_path
+ - unless cluster.enabled?
+ %span.badge.badge-danger Connection disabled
+ .table-section.section-25
+ .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Environment scope")
+ .table-mobile-content= cluster.environment_scope
+ .table-section.section-15.text-right
+ .table-mobile-header{ role: "rowheader" }
+ .table-mobile-content
+ %span.badge.badge-light
+ = cluster.project_type? ? s_("ClusterIntegration|Project cluster") : s_("ClusterIntegration|Group cluster")
diff --git a/app/views/clusters/clusters/_empty_state.html.haml b/app/views/clusters/clusters/_empty_state.html.haml
new file mode 100644
index 00000000000..c926ec258f0
--- /dev/null
+++ b/app/views/clusters/clusters/_empty_state.html.haml
@@ -0,0 +1,14 @@
+.row.empty-state
+ .col-12
+ .svg-content= image_tag 'illustrations/clusters_empty.svg'
+ .col-12
+ .text-content
+ %h4.text-center= s_('ClusterIntegration|Integrate Kubernetes cluster automation')
+ %p
+ = s_('ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way.')
+ = clusterable.empty_state_help_text
+ = clusterable.learn_more_link
+
+ - if clusterable.can_create_cluster?
+ .text-center
+ = link_to s_('ClusterIntegration|Add Kubernetes cluster'), clusterable.new_path, class: 'btn btn-success'
diff --git a/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
index 73b11d509d3..73b11d509d3 100644
--- a/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml
+++ b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
diff --git a/app/views/projects/clusters/_integration_form.html.haml b/app/views/clusters/clusters/_integration_form.html.haml
index d0a553e3414..5e451f60c9d 100644
--- a/app/views/projects/clusters/_integration_form.html.haml
+++ b/app/views/clusters/clusters/_integration_form.html.haml
@@ -1,4 +1,4 @@
-= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
+= form_for @cluster, url: clusterable.cluster_path(@cluster), as: :cluster do |field|
= form_errors(@cluster)
.form-group
%h5= s_('ClusterIntegration|Integration status')
@@ -13,7 +13,7 @@
= sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
.form-text.text-muted= s_('ClusterIntegration|Enable or disable GitLab\'s connection to your Kubernetes cluster.')
- - if has_multiple_clusters?(@project)
+ - if has_multiple_clusters?
.form-group
%h5= s_('ClusterIntegration|Environment scope')
= field.text_field :environment_scope, class: 'col-md-6 form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Environment scope')
@@ -23,7 +23,7 @@
.form-group
= field.submit _('Save changes'), class: 'btn btn-success'
- - unless has_multiple_clusters?(@project)
+ - unless has_multiple_clusters?
%h5= s_('ClusterIntegration|Environment scope')
%p
%code *
diff --git a/app/views/clusters/clusters/_sidebar.html.haml b/app/views/clusters/clusters/_sidebar.html.haml
new file mode 100644
index 00000000000..6e4415c21a9
--- /dev/null
+++ b/app/views/clusters/clusters/_sidebar.html.haml
@@ -0,0 +1,6 @@
+%h4.prepend-top-0
+ = s_('ClusterIntegration|Add a Kubernetes cluster integration')
+%p
+ = clusterable.sidebar_text
+%p
+ = clusterable.learn_more_link
diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/clusters/clusters/gcp/_form.html.haml
index 171ceeceb68..8ed4666e79a 100644
--- a/app/views/projects/clusters/gcp/_form.html.haml
+++ b/app/views/clusters/clusters/gcp/_form.html.haml
@@ -12,14 +12,14 @@
%p= link_to('Select a different Google account', @authorize_url)
-= form_for @gcp_cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: token_in_session } }, url: create_gcp_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field|
+= form_for @gcp_cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: token_in_session } }, url: clusterable.create_gcp_clusters_path, as: :cluster do |field|
= form_errors(@gcp_cluster)
.form-group
= field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold'
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
.form-group
= field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-bold'
- = field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope')
+ = field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?, placeholder: s_('ClusterIntegration|Environment scope')
= field.fields_for :provider_gcp, @gcp_cluster.provider_gcp do |provider_gcp_field|
.form-group
@@ -64,7 +64,7 @@
.form-group
.form-check
= provider_gcp_field.check_box :legacy_abac, { class: 'form-check-input' }, false, true
- = provider_gcp_field.label :legacy_abac, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
+ = provider_gcp_field.label :legacy_abac, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold'
.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.')
diff --git a/app/views/projects/clusters/gcp/_header.html.haml b/app/views/clusters/clusters/gcp/_header.html.haml
index a2ad3cd64df..a2ad3cd64df 100644
--- a/app/views/projects/clusters/gcp/_header.html.haml
+++ b/app/views/clusters/clusters/gcp/_header.html.haml
diff --git a/app/views/projects/clusters/gcp/_show.html.haml b/app/views/clusters/clusters/gcp/_show.html.haml
index 779c9c245c1..e9f05eaf453 100644
--- a/app/views/projects/clusters/gcp/_show.html.haml
+++ b/app/views/clusters/clusters/gcp/_show.html.haml
@@ -6,7 +6,7 @@
%span.input-group-append
= clipboard_button(text: @cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), class: 'input-group-text btn-default')
-= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
+= form_for @cluster, url: clusterable.cluster_path(@cluster), as: :cluster do |field|
= form_errors(@cluster)
= field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
@@ -33,14 +33,15 @@
= s_('ClusterIntegration|Show')
= clipboard_button(text: @cluster.platform_kubernetes.token, title: s_('ClusterIntegration|Copy Token'), class: 'btn-default')
- .form-group
- = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
- = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
+ - if @cluster.allow_user_defined_namespace?
+ .form-group
+ = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
+ = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
.form-group
.form-check
= platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac'
- = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
+ = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold'
.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.')
diff --git a/app/views/projects/clusters/index.html.haml b/app/views/clusters/clusters/index.html.haml
index a55de84b5cd..ad6d1d856d6 100644
--- a/app/views/projects/clusters/index.html.haml
+++ b/app/views/clusters/clusters/index.html.haml
@@ -10,14 +10,13 @@
.top-area.adjust
.nav-text
= s_("ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project")
- .ci-table.js-clusters-list
+ = render 'clusters/clusters/buttons'
+ .clusters-table.js-clusters-list
.gl-responsive-table-row.table-row-header{ role: "row" }
- .table-section.section-30{ role: "rowheader" }
+ .table-section.section-60{ role: "rowheader" }
= s_("ClusterIntegration|Kubernetes cluster")
.table-section.section-30{ role: "rowheader" }
= s_("ClusterIntegration|Environment scope")
- .table-section.section-30{ role: "rowheader" }
- = s_("ClusterIntegration|Project namespace")
.table-section.section-10{ role: "rowheader" }
- @clusters.each do |cluster|
= render "cluster", cluster: cluster.present(current_user: current_user)
diff --git a/app/views/projects/clusters/new.html.haml b/app/views/clusters/clusters/new.html.haml
index a38003f5750..eeeef6bd824 100644
--- a/app/views/projects/clusters/new.html.haml
+++ b/app/views/clusters/clusters/new.html.haml
@@ -19,9 +19,9 @@
.tab-content.gitlab-tab-content
.tab-pane{ id: 'create-gcp-cluster-pane', class: active_when(active_tab == 'gcp'), role: 'tabpanel' }
- = render 'projects/clusters/gcp/header'
+ = render 'clusters/clusters/gcp/header'
- if @valid_gcp_token
- = render 'projects/clusters/gcp/form'
+ = render 'clusters/clusters/gcp/form'
- elsif @authorize_url
.signin-with-google
= link_to(image_tag('auth_buttons/signin_with_google.png', width: '191px'), @authorize_url)
@@ -32,5 +32,5 @@
= s_('Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_to_documentation: link }
.tab-pane{ id: 'add-user-cluster-pane', class: active_when(active_tab == 'user'), role: 'tabpanel' }
- = render 'projects/clusters/user/header'
- = render 'projects/clusters/user/form'
+ = render 'clusters/clusters/user/header'
+ = render 'clusters/clusters/user/form'
diff --git a/app/views/projects/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml
index eddd3613c5f..8a7f7a5c978 100644
--- a/app/views/projects/clusters/show.html.haml
+++ b/app/views/clusters/clusters/show.html.haml
@@ -1,24 +1,27 @@
- @content_class = "limit-container-width" unless fluid_layout
-- add_to_breadcrumbs "Kubernetes Clusters", project_clusters_path(@project)
+- add_to_breadcrumbs "Kubernetes Clusters", clusterable.index_path
- breadcrumb_title @cluster.name
- page_title _("Kubernetes Cluster")
+- manage_prometheus_path = edit_project_service_path(@cluster.project, 'prometheus') if @project
- expanded = Rails.env.test?
-- status_path = status_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster.id, format: :json) if can?(current_user, :admin_cluster, @cluster)
+- 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,
- install_helm_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :helm),
- install_ingress_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :ingress),
- install_prometheus_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :prometheus),
- install_runner_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :runner),
- install_jupyter_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :jupyter),
+ install_helm_path: clusterable.install_applications_cluster_path(@cluster, :helm),
+ install_ingress_path: clusterable.install_applications_cluster_path(@cluster, :ingress),
+ install_prometheus_path: clusterable.install_applications_cluster_path(@cluster, :prometheus),
+ install_runner_path: clusterable.install_applications_cluster_path(@cluster, :runner),
+ install_jupyter_path: clusterable.install_applications_cluster_path(@cluster, :jupyter),
+ install_knative_path: clusterable.install_applications_cluster_path(@cluster, :knative),
toggle_status: @cluster.enabled? ? 'true': 'false',
+ cluster_type: @cluster.cluster_type,
cluster_status: @cluster.status_name,
cluster_status_reason: @cluster.status_reason,
help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications'),
ingress_help_path: help_page_path('user/project/clusters/index.md', anchor: 'getting-the-external-ip-address'),
ingress_dns_help_path: help_page_path('topics/autodevops/quick_start_guide.md', anchor: 'point-dns-at-cluster-ip'),
- manage_prometheus_path: edit_project_service_path(@cluster.project, 'prometheus') } }
+ manage_prometheus_path: manage_prometheus_path } }
.js-cluster-application-notice
.flash-container
@@ -38,9 +41,9 @@
%p= s_('ClusterIntegration|See and edit the details for your Kubernetes cluster')
.settings-content
- if @cluster.managed?
- = render 'projects/clusters/gcp/show'
+ = render 'clusters/clusters/gcp/show'
- else
- = render 'projects/clusters/user/show'
+ = render 'clusters/clusters/user/show'
%section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) }
.settings-header
diff --git a/app/views/projects/clusters/user/_form.html.haml b/app/views/clusters/clusters/user/_form.html.haml
index 54a6e685bb0..9793c77fc2b 100644
--- a/app/views/projects/clusters/user/_form.html.haml
+++ b/app/views/clusters/clusters/user/_form.html.haml
@@ -1,9 +1,9 @@
-= form_for @user_cluster, url: create_user_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field|
+= form_for @user_cluster, url: clusterable.create_user_clusters_path, as: :cluster do |field|
= form_errors(@user_cluster)
.form-group
= field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold'
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
- - if has_multiple_clusters?(@project)
+ - if has_multiple_clusters?
.form-group
= field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-bold'
= field.text_field :environment_scope, class: 'form-control', placeholder: s_('ClusterIntegration|Environment scope')
@@ -21,14 +21,15 @@
= platform_kubernetes_field.label :token, s_('ClusterIntegration|Token'), class: 'label-bold'
= platform_kubernetes_field.text_field :token, class: 'form-control', placeholder: s_('ClusterIntegration|Service token'), autocomplete: 'off'
- .form-group
- = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold'
- = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
+ - if @user_cluster.allow_user_defined_namespace?
+ .form-group
+ = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold'
+ = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
.form-group
.form-check
= platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input qa-rbac-checkbox' }, 'rbac', 'abac'
- = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
+ = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold'
.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.')
diff --git a/app/views/projects/clusters/user/_header.html.haml b/app/views/clusters/clusters/user/_header.html.haml
index 749177fa6c1..749177fa6c1 100644
--- a/app/views/projects/clusters/user/_header.html.haml
+++ b/app/views/clusters/clusters/user/_header.html.haml
diff --git a/app/views/projects/clusters/user/_show.html.haml b/app/views/clusters/clusters/user/_show.html.haml
index 5b57f7ceb7d..cac8e72edd3 100644
--- a/app/views/projects/clusters/user/_show.html.haml
+++ b/app/views/clusters/clusters/user/_show.html.haml
@@ -1,4 +1,4 @@
-= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
+= form_for @cluster, url: clusterable.cluster_path(@cluster), as: :cluster do |field|
= form_errors(@cluster)
.form-group
= field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold'
@@ -22,14 +22,15 @@
%button.js-show-cluster-token.btn-blank{ type: 'button' }
= s_('ClusterIntegration|Show')
- .form-group
- = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold'
- = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
+ - if @cluster.allow_user_defined_namespace?
+ .form-group
+ = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold'
+ = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
.form-group
.form-check
= platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac'
- = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
+ = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold'
.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.')
diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml
index b11dc2c8e9b..6eb067da95c 100644
--- a/app/views/dashboard/snippets/index.html.haml
+++ b/app/views/dashboard/snippets/index.html.haml
@@ -5,9 +5,4 @@
= render 'dashboard/snippets_head'
= render partial: 'snippets/snippets_scope_menu', locals: { include_private: true }
-.d-block.d-sm-none
- &nbsp;
- = link_to new_snippet_path, class: "btn btn-success btn-block", title: "New snippet" do
- New snippet
-
= render partial: 'snippets/snippets', locals: { link_project: true }
diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml
index 1765251c93d..10187129a33 100644
--- a/app/views/discussions/_discussion.html.haml
+++ b/app/views/discussions/_discussion.html.haml
@@ -1,12 +1,12 @@
- expanded = discussion.expanded?
-%li.note.note-discussion.timeline-entry
+%li.note.note-discussion.timeline-entry.unstyled-comments
.timeline-entry-inner
- .timeline-icon
- = link_to user_path(discussion.author) do
- = image_tag avatar_icon_for_user(discussion.author), class: "avatar s40"
.timeline-content
.discussion.js-toggle-container{ data: { discussion_id: discussion.id } }
.discussion-header
+ .timeline-icon
+ = link_to user_path(discussion.author) do
+ = image_tag avatar_icon_for_user(discussion.author), class: "avatar s40"
.discussion-actions
%button.note-action-button.discussion-toggle-button.js-toggle-button{ type: "button", class: ("js-toggle-lazy-diff" unless expanded) }
- if expanded
diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml
index 78a1d1a0553..2fcb1d1fd2b 100644
--- a/app/views/events/_event.html.haml
+++ b/app/views/events/_event.html.haml
@@ -1,5 +1,5 @@
- if event.visible_to_user?(current_user)
- .event-item{ class: event_row_class(event) }
+ .event-item
.event-item-timestamp
#{time_ago_with_tooltip(event.created_at)}
diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml
index 829a3da1558..96d6553a2ac 100644
--- a/app/views/events/event/_common.html.haml
+++ b/app/views/events/event/_common.html.haml
@@ -1,20 +1,19 @@
= icon_for_profile_event(event)
-.event-title
- %span.author_name= link_to_author(event)
- %span{ class: event.action_name }
+= event_user_info(event)
+
+.event-title.d-flex.flex-wrap
+ = inline_event_icon(event)
- if event.target
- = event.action_name
- %strong
- = link_to [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title do
- = event.target_type.titleize.downcase
- = event.target.reference_link_text
+ %span.event-type.d-inline-block.append-right-4{ class: event.action_name }
+ = event.action_name
+ %span.event-target-type.append-right-4= event.target_type.titleize.downcase
+ = 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
- else
- = event_action_name(event)
+ %span.event-type.d-inline-block.append-right-4{ class: event.action_name }
+ = event_action_name(event)
= render "events/event_scope", event: event
-
-- if event.target.respond_to?(:title)
- .event-body
- .event-note
- = event.target.title
diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml
index 6ad7e157131..2f156603414 100644
--- a/app/views/events/event/_created_project.html.haml
+++ b/app/views/events/event/_created_project.html.haml
@@ -1,8 +1,10 @@
= icon_for_profile_event(event)
-.event-title
- %span.author_name= link_to_author(event)
- %span{ class: event.action_name }
+= event_user_info(event)
+
+.event-title.d-flex.flex-wrap
+ = inline_event_icon(event)
+ %span.event-type.d-inline-block.append-right-4{ class: event.action_name }
= event_action_name(event)
- if event.project
diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml
index cdacd998a69..fb0d2c3b8b0 100644
--- a/app/views/events/event/_note.html.haml
+++ b/app/views/events/event/_note.html.haml
@@ -1,9 +1,13 @@
= icon_for_profile_event(event)
-.event-title
- %span.author_name= link_to_author(event)
- = event.action_name
+= event_user_info(event)
+
+.event-title.d-flex.flex-wrap
+ = inline_event_icon(event)
+ %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
= render "events/event_scope", event: event
diff --git a/app/views/events/event/_private.html.haml b/app/views/events/event/_private.html.haml
index ccd2aacb4ea..d91f30c07cb 100644
--- a/app/views/events/event/_private.html.haml
+++ b/app/views/events/event/_private.html.haml
@@ -1,10 +1,11 @@
-.event-inline.event-item
+.event-item
.event-item-timestamp
= time_ago_with_tooltip(event.created_at)
- .system-note-image= sprite_icon('eye-slash', size: 16, css_class: 'icon')
+ .system-note-image= sprite_icon('eye-slash', size: 24, css_class: 'icon')
- .event-title
- - author_name = capture do
- %span.author_name= link_to_author(event)
- = s_('Profiles|%{author_name} made a private contribution').html_safe % { author_name: author_name }
+ = event_user_info(event)
+
+ .event-title.d-flex.flex-wrap
+ = inline_event_icon(event)
+ = s_('Profiles|Made a private contribution')
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index 5f0ee79cd9b..69914fccc48 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -2,10 +2,12 @@
= icon_for_profile_event(event)
-.event-title
- %span.author_name= link_to_author(event)
- %span.pushed #{event.action_name} #{event.ref_type}
- %strong
+= event_user_info(event)
+
+.event-title.d-flex.flex-wrap
+ = inline_event_icon(event)
+ %span.event-type.d-inline-block.append-right-4.pushed #{event.action_name} #{event.ref_type}
+ %span.append-right-4
- commits_link = project_commits_path(project, event.ref_name)
- should_link = event.tag? ? project.repository.tag_exists?(event.ref_name) : project.repository.branch_exists?(event.ref_name)
= link_to_if should_link, event.ref_name, commits_link, class: 'ref-name'
diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml
index 5b78ce910b8..4df3d831942 100644
--- a/app/views/groups/labels/index.html.haml
+++ b/app/views/groups/labels/index.html.haml
@@ -6,7 +6,7 @@
- subscribed = params[:subscribed]
- labels_or_filters = @labels.exists? || search.present? || subscribed.present?
-- if can_admin_label
+- if @labels.present? && can_admin_label
- content_for(:header_content) do
.nav-controls
= link_to _('New label'), new_group_label_path(@group), class: "btn btn-success"
diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml
index 684b51b8552..51dcc9d0cda 100644
--- a/app/views/groups/new.html.haml
+++ b/app/views/groups/new.html.haml
@@ -1,12 +1,12 @@
- @hide_breadcrumbs = true
- @hide_top_links = true
-- page_title 'New Group'
-- header_title "Groups", dashboard_groups_path
+- page_title _('New Group')
+- header_title _("Groups"), dashboard_groups_path
+.page-title-holder
+ %h1.page-title= _('New group')
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
- %h4.prepend-top-0
- = _('New group')
%p
- group_docs_path = help_page_path('user/group/index')
- group_docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: group_docs_path }
@@ -15,24 +15,29 @@
- subgroup_docs_path = help_page_path('user/group/subgroups/index')
- subgroup_docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: subgroup_docs_path }
= s_('Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}.').html_safe % { subgroup_docs_link_start: subgroup_docs_link_start, subgroup_docs_link_end: '</a>'.html_safe }
+ %p
+ = _('Projects that belong to a group are prefixed with the group namespace. Existing projects may be moved into a group.')
.col-lg-9
= form_for @group, html: { class: 'group-form gl-show-field-errors' } do |f|
= form_errors(@group)
= render 'shared/group_form', f: f, autofocus: true
- .form-group.row.group-description-holder
- = f.label :avatar, "Group avatar", class: 'col-form-label col-sm-2'
- .col-sm-10
- = render 'shared/choose_group_avatar_button', f: f
-
- = render 'shared/old_visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group, with_label: false
+ .row
+ .form-group.group-description-holder.col-sm-12
+ = f.label :avatar, _("Group avatar"), class: 'label-bold'
+ %div
+ = render 'shared/choose_group_avatar_button', f: f
- = render 'create_chat_team', f: f if Gitlab.config.mattermost.enabled
+ .form-group.col-sm-12
+ %label.label-bold
+ = _('Visibility level')
+ %p
+ = _('Who will be able to see this group?')
+ = link_to _('View the documentation'), help_page_path("public_access/public_access"), target: '_blank'
+ = render 'shared/visibility_level', f: f, visibility_level: default_group_visibility, can_change_visibility_level: true, form_model: @group, with_label: false
- .form-group.row
- .offset-sm-2.col-sm-10
- = render 'shared/group_tips'
+ = render 'create_chat_team', f: f if Gitlab.config.mattermost.enabled
.form-actions
= f.submit 'Create group', class: "btn btn-success"
diff --git a/app/views/import/bitbucket_server/status.html.haml b/app/views/import/bitbucket_server/status.html.haml
index ae09e0dfa18..56d4f2ba881 100644
--- a/app/views/import/bitbucket_server/status.html.haml
+++ b/app/views/import/bitbucket_server/status.html.haml
@@ -84,4 +84,6 @@
= link_to 'import flow', status_import_bitbucket_server_path
again.
+= paginate_without_count(@collection)
+
.js-importer-status{ data: { jobs_import_path: "#{jobs_import_bitbucket_server_path}", import_path: "#{import_bitbucket_server_path}" } }
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index b7d69539eb7..474ef25cef7 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -71,7 +71,7 @@
= link_to admin_impersonation_path, class: 'nav-link impersonation-btn', method: :delete, title: _('Stop impersonation'), aria: { label: _('Stop impersonation') }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= icon('user-secret')
- if header_link?(:sign_in)
- %li.nav-item
+ %li.nav-item.m-auto
%div
- sign_in_text = allow_signup? ? _('Sign in / Register') : _('Sign in')
= link_to sign_in_text, new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in'
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 8f8b6b454d9..ea5f2b166b4 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -64,7 +64,7 @@
= link_to '#', class: 'dashboard-shortcuts-web-ide', title: _('Web IDE') do
= _('Web IDE')
- - if Gitlab::Sherlock.enabled? || can?(current_user, :read_instance_statistics)
+ - if show_separator?
%li.line-separator.d-none.d-sm-block
= render_if_exists 'dashboard/operations/nav_link'
- if can?(current_user, :read_instance_statistics)
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index 163556f4509..3cd5168c1f7 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -74,6 +74,8 @@
%span
= boards_link_text
+ = render_if_exists 'layouts/nav/issues_analytics_link'
+
- if group_sidebar_link?(:labels)
= nav_link(path: 'labels#index') do
= link_to group_labels_path(@group), title: _('Labels') do
@@ -114,6 +116,19 @@
%strong.fly-out-top-item-name
= _('Members')
+ - if group_sidebar_link?(:kubernetes)
+ = nav_link(controller: [:clusters]) do
+ = link_to group_clusters_path(@group) 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 group_clusters_path(@group), title: _('Kubernetes'), class: 'shortcuts-kubernetes' do
+ %strong.fly-out-top-item-name
+ = _('Kubernetes')
+
- if group_sidebar_link?(:settings)
= nav_link(path: group_nav_link_paths) do
= link_to edit_group_path(@group) do
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 174033f3d49..ab15889a465 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -195,7 +195,7 @@
= _('Charts')
- if project_nav_tab? :operations
- = nav_link(controller: [:environments, :clusters, :user, :gcp]) do
+ = nav_link(controller: sidebar_operations_paths) do
= link_to metrics_project_environments_path(@project), class: 'shortcuts-operations' do
.nav-icon-container
= sprite_icon('cloud-gear')
@@ -203,7 +203,7 @@
= _('Operations')
%ul.sidebar-sub-level-items
- = nav_link(controller: [:environments, :clusters, :user, :gcp], html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: sidebar_operations_paths, html_options: { class: "fly-out-top-item" } ) do
= link_to metrics_project_environments_path(@project) do
%strong.fly-out-top-item-name
= _('Operations')
@@ -215,6 +215,8 @@
%span
= _('Metrics')
+ = render_if_exists "layouts/nav/sidebar/tracing_link"
+
= nav_link(controller: :environments, action: [:index, :folder, :show, :new, :edit, :create, :update, :stop, :terminal]) do
= link_to project_environments_path(@project), title: _('Environments'), class: 'shortcuts-environments qa-operations-environments-link' do
%span
@@ -326,6 +328,7 @@
= link_to project_settings_ci_cd_path(@project), title: _('CI / CD') do
%span
= _('CI / CD')
+ = render_if_exists 'projects/sidebar/settings_operations'
- if @project.pages_available?
= nav_link(controller: :pages) do
= link_to project_pages_path(@project), title: _('Pages') do
diff --git a/app/views/notify/changed_milestone_issue_email.html.haml b/app/views/notify/changed_milestone_issue_email.html.haml
new file mode 100644
index 00000000000..7d5425fc72d
--- /dev/null
+++ b/app/views/notify/changed_milestone_issue_email.html.haml
@@ -0,0 +1,3 @@
+%p
+ Milestone changed to
+ %strong= link_to(@milestone.name, @milestone_url)
diff --git a/app/views/notify/changed_milestone_issue_email.text.erb b/app/views/notify/changed_milestone_issue_email.text.erb
new file mode 100644
index 00000000000..c5fc0b61518
--- /dev/null
+++ b/app/views/notify/changed_milestone_issue_email.text.erb
@@ -0,0 +1 @@
+Milestone changed to <%= @milestone.name %> ( <%= @milestone_url %> )
diff --git a/app/views/notify/changed_milestone_merge_request_email.html.haml b/app/views/notify/changed_milestone_merge_request_email.html.haml
new file mode 100644
index 00000000000..7d5425fc72d
--- /dev/null
+++ b/app/views/notify/changed_milestone_merge_request_email.html.haml
@@ -0,0 +1,3 @@
+%p
+ Milestone changed to
+ %strong= link_to(@milestone.name, @milestone_url)
diff --git a/app/views/notify/changed_milestone_merge_request_email.text.erb b/app/views/notify/changed_milestone_merge_request_email.text.erb
new file mode 100644
index 00000000000..c5fc0b61518
--- /dev/null
+++ b/app/views/notify/changed_milestone_merge_request_email.text.erb
@@ -0,0 +1 @@
+Milestone changed to <%= @milestone.name %> ( <%= @milestone_url %> )
diff --git a/app/views/notify/removed_milestone_issue_email.html.haml b/app/views/notify/removed_milestone_issue_email.html.haml
new file mode 100644
index 00000000000..7e9205b6491
--- /dev/null
+++ b/app/views/notify/removed_milestone_issue_email.html.haml
@@ -0,0 +1,2 @@
+%p
+ Milestone removed
diff --git a/app/views/notify/removed_milestone_issue_email.text.erb b/app/views/notify/removed_milestone_issue_email.text.erb
new file mode 100644
index 00000000000..0b83ed7a4c5
--- /dev/null
+++ b/app/views/notify/removed_milestone_issue_email.text.erb
@@ -0,0 +1 @@
+Milestone removed
diff --git a/app/views/notify/removed_milestone_merge_request_email.html.haml b/app/views/notify/removed_milestone_merge_request_email.html.haml
new file mode 100644
index 00000000000..7e9205b6491
--- /dev/null
+++ b/app/views/notify/removed_milestone_merge_request_email.html.haml
@@ -0,0 +1,2 @@
+%p
+ Milestone removed
diff --git a/app/views/notify/removed_milestone_merge_request_email.text.erb b/app/views/notify/removed_milestone_merge_request_email.text.erb
new file mode 100644
index 00000000000..0b83ed7a4c5
--- /dev/null
+++ b/app/views/notify/removed_milestone_merge_request_email.text.erb
@@ -0,0 +1 @@
+Milestone removed
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 156c0d05b02..7c378633667 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -46,7 +46,7 @@
Layout width
= f.select :layout, layout_choices, {}, class: 'form-control'
.form-text.text-muted
- Choose between fixed (max. 1200px) and fluid (100%) application layout.
+ Choose between fixed (max. 1280px) and fluid (100%) application layout.
.form-group
= f.label :dashboard, class: 'label-bold' do
Default dashboard
@@ -56,6 +56,6 @@
Project overview content
= f.select :project_view, project_view_choices, {}, class: 'form-control'
.form-text.text-muted
- Choose what content you want to see on a project’s overview page
+ Choose what content you want to see on a project’s overview page.
.form-group
= f.submit 'Save changes', class: 'btn btn-success'
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index ea215e3e718..2603c558c0f 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -91,8 +91,9 @@
= f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email),
{ help: s_("Profiles|This email will be displayed on your public profile."), include_blank: s_("Profiles|Do not show on profile") },
control_class: 'select2'
- = f.select :commit_email, options_for_select(@user.verified_emails, selected: @user.commit_email),
- { help: 'This email will be used for web based operations, such as edits and merges.' },
+ - commit_email_docs_link = link_to s_('Profiles|Learn more'), help_page_path('user/profile/index', anchor: 'commit-email', target: '_blank')
+ = f.select :commit_email, options_for_select(commit_email_select_options(@user), selected: selected_commit_email(@user)),
+ { help: s_("Profiles|This email will be used for web based operations, such as edits and merges. %{learn_more}").html_safe % { learn_more: commit_email_docs_link } },
control_class: 'select2'
= f.select :preferred_language, Gitlab::I18n::AVAILABLE_LANGUAGES.map { |value, label| [label, value] },
{ help: s_("Profiles|This feature is experimental and translations are not complete yet.") },
diff --git a/app/views/projects/blob/viewers/_highlight_embed.html.haml b/app/views/projects/blob/viewers/_highlight_embed.html.haml
index 9bd4ef6ad0b..6a631fef1a9 100644
--- a/app/views/projects/blob/viewers/_highlight_embed.html.haml
+++ b/app/views/projects/blob/viewers/_highlight_embed.html.haml
@@ -4,4 +4,6 @@
- blob.data.each_line.each_with_index do |_, index|
%span.diff-line-num= index + 1
.blob-content{ data: { blob_id: blob.id } }
- = highlight(blob.path, blob.data, repository: nil, plain: blob.no_highlighting?)
+ %pre.code.highlight
+ %code
+ = blob.present.highlight
diff --git a/app/views/projects/blob/viewers/_text.html.haml b/app/views/projects/blob/viewers/_text.html.haml
index a91df321ca0..26ad23da436 100644
--- a/app/views/projects/blob/viewers/_text.html.haml
+++ b/app/views/projects/blob/viewers/_text.html.haml
@@ -1 +1 @@
-= render 'shared/file_highlight', blob: viewer.blob, repository: @repository
+= render 'shared/file_highlight', blob: viewer.blob
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index f5685d3b50d..0b10c66777a 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -105,10 +105,10 @@
= icon('remove', class: 'cred')
- elsif job.scheduled?
.btn-group
- .btn.btn-default.has-tooltip{ disabled: true,
- title: job.scheduled_at }
+ .btn.btn-default{ disabled: true }
= sprite_icon('planning')
- = duration_in_numbers(job.execute_in)
+ %time.js-remaining-time{ datetime: job.scheduled_at.utc.iso8601 }
+ = duration_in_numbers(job.execute_in)
- confirmation_message = s_("DelayedJobs|Are you sure you want to run %{job_name} immediately? This job will run automatically after it's timer finishes.") % { job_name: job.name }
= link_to play_project_job_path(job.project, job, return_to: request.original_url),
method: :post,
diff --git a/app/views/projects/clusters/_cluster.html.haml b/app/views/projects/clusters/_cluster.html.haml
deleted file mode 100644
index 2d7f7c6b1fb..00000000000
--- a/app/views/projects/clusters/_cluster.html.haml
+++ /dev/null
@@ -1,24 +0,0 @@
-.gl-responsive-table-row
- .table-section.section-30
- .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Kubernetes cluster")
- .table-mobile-content
- = link_to cluster.name, namespace_project_cluster_path(@project.namespace, @project, cluster)
- .table-section.section-30
- .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Environment scope")
- .table-mobile-content= cluster.environment_scope
- .table-section.section-30
- .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Project namespace")
- .table-mobile-content= cluster.platform_kubernetes&.actual_namespace
- .table-section.section-10
- .table-mobile-header{ role: "rowheader" }
- .table-mobile-content
- %button.js-project-feature-toggle.project-feature-toggle{ type: "button",
- class: "#{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}",
- "aria-label": s_("ClusterIntegration|Toggle Kubernetes Cluster"),
- disabled: !cluster.can_toggle_cluster?,
- data: { endpoint: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } }
- %input.js-project-feature-toggle-input{ type: "hidden", value: cluster.enabled? }
- = icon("spinner spin", class: "loading-icon")
- %span.toggle-icon
- = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
- = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
diff --git a/app/views/projects/clusters/_empty_state.html.haml b/app/views/projects/clusters/_empty_state.html.haml
deleted file mode 100644
index b8a3556a206..00000000000
--- a/app/views/projects/clusters/_empty_state.html.haml
+++ /dev/null
@@ -1,12 +0,0 @@
-.row.empty-state
- .col-12
- .svg-content= image_tag 'illustrations/clusters_empty.svg'
- .col-12
- .text-content
- %h4.text-center= s_('ClusterIntegration|Integrate Kubernetes cluster automation')
- - link_to_help_page = link_to(_('Learn more about Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
- %p= s_('ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page}
-
- - if can?(current_user, :create_cluster, @project)
- .text-center
- = link_to s_('ClusterIntegration|Add Kubernetes cluster'), new_project_cluster_path(@project), class: 'btn btn-success'
diff --git a/app/views/projects/clusters/_sidebar.html.haml b/app/views/projects/clusters/_sidebar.html.haml
deleted file mode 100644
index 3d10348212f..00000000000
--- a/app/views/projects/clusters/_sidebar.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-- clusters_help_url = help_page_path('user/project/clusters/index.md')
-- help_link_start = "<a href=\"%{url}\" target=\"_blank\" rel=\"noopener noreferrer\">".html_safe
-- help_link_end = '</a>'.html_safe
-%h4.prepend-top-0
- = s_('ClusterIntegration|Kubernetes cluster integration')
-%p
- = s_('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.')
-%p
- = s_('ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}.').html_safe % { help_link_start: help_link_start % { url: clusters_help_url }, help_link_end: help_link_end }
diff --git a/app/views/projects/deployments/_rollback.haml b/app/views/projects/deployments/_rollback.haml
index 281e042c915..1bd538a08ff 100644
--- a/app/views/projects/deployments/_rollback.haml
+++ b/app/views/projects/deployments/_rollback.haml
@@ -1,4 +1,4 @@
-- if can?(current_user, :create_deployment, deployment) && deployment.deployable
+- if can?(current_user, :create_deployment, deployment)
- tooltip = deployment.last? ? s_('Environments|Re-deploy to environment') : s_('Environments|Rollback environment')
= link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build has-tooltip', title: tooltip do
- if deployment.last?
diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml
index e8a5e63e59e..bc9f6c71fa8 100644
--- a/app/views/projects/diffs/_text_file.html.haml
+++ b/app/views/projects/diffs/_text_file.html.haml
@@ -3,7 +3,7 @@
.suppressed-container
%a.show-suppressed-diff.js-show-suppressed-diff Changes suppressed. Click to show.
-%table.text-file.diff-wrap-lines.code.js-syntax-highlight{ data: diff_view_data, class: too_big ? 'hide' : '' }
+%table.text-file.diff-wrap-lines.code.js-syntax-highlight.commit-diff{ data: diff_view_data, class: too_big ? 'hide' : '' }
= render partial: "projects/diffs/line",
collection: diff_file.highlighted_diff_lines,
as: :line,
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 07fc9e1c682..de768696fe9 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -148,7 +148,7 @@
= link_to 'Archive project', archive_project_path(@project),
data: { confirm: "Are you sure that you want to archive this project?" },
method: :post, class: "btn btn-warning"
- .sub-section.rename-respository
+ .sub-section.rename-repository
%h4.warning-title
Rename repository
= render 'projects/errors'
@@ -165,7 +165,7 @@
.input-group
.input-group-prepend
.input-group-text
- #{URI.join(root_url, @project.namespace.full_path)}/
+ #{Gitlab::Utils.append_path(root_url, @project.namespace.full_path)}/
= f.text_field :path, class: 'form-control'
%ul
%li Be careful. Renaming a project's repository can have unintended side effects.
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 75f35360e5e..936900a0087 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -36,7 +36,7 @@
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
- .nav-links.scrolling-tabs
+ .nav-links.scrolling-tabs.quick-links
= render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors
= render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 06ee883d6dc..2c6484c2c99 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -5,7 +5,7 @@
- subscribed = params[:subscribed]
- labels_or_filters = @labels.exists? || @prioritized_labels.exists? || search.present? || subscribed.present?
-- if can_admin_label
+- if @labels.present? && can_admin_label
- content_for(:header_content) do
.nav-controls
= link_to _('New label'), new_project_label_path(@project), class: "btn btn-success qa-label-create-new"
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index 1bf42ded97a..3cd83feb842 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -37,6 +37,6 @@
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request'
- if can_update_merge_request
- = link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "d-none d-sm-none d-md-block btn btn-grouped js-issuable-edit"
+ = link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "d-none d-sm-none d-md-block btn btn-grouped js-issuable-edit qa-edit-button"
= render 'shared/issuable/close_reopen_button', issuable: @merge_request, can_update: can_update_merge_request, can_reopen: can_update_merge_request
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index efc2d88172e..515499956a2 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -21,6 +21,7 @@
window.gl.mrWidgetData = #{serialize_issuable(@merge_request, serializer: 'widget')}
window.gl.mrWidgetData.squash_before_merge_help_path = '#{help_page_path("user/project/merge_requests/squash_and_merge")}';
+ window.gl.mrWidgetData.troubleshooting_docs_path = '#{help_page_path('user/project/merge_requests', anchor: 'troubleshooting')}';
#js-vue-mr-widget.mr-widget
@@ -33,7 +34,7 @@
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
%ul.merge-request-tabs.nav-tabs.nav.nav-links.scrolling-tabs
- %li.notes-tab
+ %li.notes-tab.qa-notes-tab
= tab_link_for @merge_request, :show, force_link: @commit.present? do
Discussion
%span.badge.badge-pill= @merge_request.related_notes.user.count
@@ -47,7 +48,7 @@
= tab_link_for @merge_request, :pipelines do
Pipelines
%span.badge.badge-pill.js-pipelines-mr-count= @pipelines.size
- %li.diffs-tab
+ %li.diffs-tab.qa-diffs-tab
= tab_link_for @merge_request, :diffs do
Changes
%span.badge.badge-pill= @merge_request.diff_size
diff --git a/app/views/projects/notes/_more_actions_dropdown.html.haml b/app/views/projects/notes/_more_actions_dropdown.html.haml
index 88085c7185b..8de84f82e9f 100644
--- a/app/views/projects/notes/_more_actions_dropdown.html.haml
+++ b/app/views/projects/notes/_more_actions_dropdown.html.haml
@@ -11,8 +11,9 @@
- unless is_current_user
%li
= link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do
- Report as abuse
+ = _('Report abuse to GitLab')
- if note_editable
%li
= link_to note_url(note), method: :delete, data: { confirm: 'Are you sure you want to delete this comment?' }, remote: true, class: 'js-note-delete' do
- %span.text-danger Delete comment
+ %span.text-danger
+ = _('Delete comment')
diff --git a/app/views/projects/pages/_https_only.html.haml b/app/views/projects/pages/_https_only.html.haml
index 57345edb90b..ce3ef29c32e 100644
--- a/app/views/projects/pages/_https_only.html.haml
+++ b/app/views/projects/pages/_https_only.html.haml
@@ -1,9 +1,9 @@
= form_for @project, url: namespace_project_pages_path(@project.namespace.becomes(Namespace), @project), html: { class: 'inline', title: pages_https_only_title } do |f|
- = f.check_box :pages_https_only, class: 'float-left', disabled: pages_https_only_disabled?
-
- .prepend-left-20
- = f.label :pages_https_only, class: pages_https_only_label_class do
- %strong Force domains with SSL certificates to use HTTPS
+ .form-group
+ .form-check
+ = f.check_box :pages_https_only, class: 'form-check-input', disabled: pages_https_only_disabled?
+ = f.label :pages_https_only, class: pages_https_only_label_class do
+ %strong Force domains with SSL certificates to use HTTPS
- unless pages_https_only_disabled?
.prepend-top-10
diff --git a/app/views/projects/pages/_list.html.haml b/app/views/projects/pages/_list.html.haml
index e7178f9160c..2427b4d7611 100644
--- a/app/views/projects/pages/_list.html.haml
+++ b/app/views/projects/pages/_list.html.haml
@@ -4,9 +4,9 @@
.card
.card-header
Domains (#{@domains.count})
- %ul.content-list.pages-domain-list{ class: ("has-verification-status" if verification_enabled) }
+ %ul.list-group.list-group-flush.pages-domain-list{ class: ("has-verification-status" if verification_enabled) }
- @domains.each do |domain|
- %li.pages-domain-list-item.unstyled
+ %li.pages-domain-list-item.list-group-item.d-flex.justify-content-between
- if verification_enabled
- tooltip, status = domain.unverified? ? [_('Unverified'), 'failed'] : [_('Verified'), 'success']
.domain-status.ci-status-icon.has-tooltip{ class: "ci-status-icon-#{status}", title: tooltip }
@@ -16,7 +16,7 @@
= domain.url
= icon('external-link')
- if domain.subject
- %p
+ %div
%span.badge.badge-gray Certificate: #{domain.subject}
- if domain.expired?
%span.badge.badge-danger Expired
@@ -24,6 +24,6 @@
= link_to 'Details', project_pages_domain_path(@project, domain), class: "btn btn-sm btn-grouped"
= link_to 'Remove', project_pages_domain_path(@project, domain), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped"
- if verification_enabled && domain.unverified?
- %li.warning-row
+ %li.list-group-item.bs-callout-warning
#{domain.domain} is not verified. To learn how to verify ownership, visit your
#{link_to 'domain details', project_pages_domain_path(@project, domain)}.
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index 95bba47802c..66e202103a9 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -61,12 +61,14 @@
%td.responsive-table-cell.build-failure{ data: { column: _("Failure")} }
= build.present.callout_failure_message
%td.responsive-table-cell.build-actions
- = link_to retry_project_job_path(build.project, build, return_to: request.original_url), method: :post, title: _('Retry'), class: 'btn btn-build' do
- = icon('repeat')
- %tr.build-trace-row.responsive-table-border-end
- %td
- %td.responsive-table-cell.build-trace-container{ colspan: 4 }
- %pre.build-trace.build-trace-rounded
- %code.bash.js-build-output
- = build_summary(build)
+ - if can?(current_user, :update_build, job)
+ = link_to retry_project_job_path(build.project, build, return_to: request.original_url), method: :post, title: _('Retry'), class: 'btn btn-build' do
+ = icon('repeat')
+ - if can?(current_user, :read_build, job)
+ %tr.build-trace-row.responsive-table-border-end
+ %td
+ %td.responsive-table-cell.build-trace-container{ colspan: 4 }
+ %pre.build-trace.build-trace-rounded
+ %code.bash.js-build-output
+ = build_summary(build)
= render_if_exists "projects/pipelines/tabs_content", pipeline: @pipeline, project: @project
diff --git a/app/views/projects/settings/ci_cd/_badge.html.haml b/app/views/projects/settings/ci_cd/_badge.html.haml
index d14360913a4..82c8ec088e5 100644
--- a/app/views/projects/settings/ci_cd/_badge.html.haml
+++ b/app/views/projects/settings/ci_cd/_badge.html.haml
@@ -15,14 +15,14 @@
.col-md-2.text-center
Markdown
.col-md-10.code.js-syntax-highlight
- = highlight('.md', badge.to_markdown)
+ = highlight('.md', badge.to_markdown, language: 'markdown')
.row
%hr
.row
.col-md-2.text-center
HTML
.col-md-10.code.js-syntax-highlight
- = highlight('.html', badge.to_html)
+ = highlight('.html', badge.to_html, language: 'html')
.row
%hr
.row
diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml
index 41afaa9ffc0..621b7922072 100644
--- a/app/views/projects/settings/ci_cd/_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_form.html.haml
@@ -91,7 +91,7 @@
%code \(\d+.\d+\%\) covered
%li
pytest-cov (Python) -
- %code \d+\%\s*$
+ %code ^TOTAL\s+\d+\s+\d+\s+(\d+\%)$
%li
phpunit --coverage-text --colors=never (PHP) -
%code ^\s*Lines:\s*\d+.\d+\%
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 283031b06da..f29ce4f5c06 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -22,7 +22,7 @@
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
- .nav-links.scrolling-tabs
+ .nav-links.scrolling-tabs.quick-links
= render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout)
= render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout)
diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml
deleted file mode 100644
index f79f3af36d4..00000000000
--- a/app/views/projects/tree/_blob_item.html.haml
+++ /dev/null
@@ -1,12 +0,0 @@
-- is_lfs_blob = @lfs_blob_ids.include?(blob_item.id)
-%tr{ class: "tree-item #{tree_hex_class(blob_item)}" }
- %td.tree-item-file-name
- = tree_icon(type, blob_item.mode, blob_item.name)
- - file_name = blob_item.name
- = link_to project_blob_path(@project, tree_join(@id || @commit.id, blob_item.name)), class: 'str-truncated', title: file_name do
- %span= file_name
- - if is_lfs_blob
- %span.badge.label-lfs.prepend-left-5 LFS
- %td.d-none.d-sm-table-cell.tree-commit
- %td.tree-time-ago.cgray.text-right
- = render 'projects/tree/spinner'
diff --git a/app/views/projects/tree/_spinner.html.haml b/app/views/projects/tree/_spinner.html.haml
deleted file mode 100644
index b47ad0f41e4..00000000000
--- a/app/views/projects/tree/_spinner.html.haml
+++ /dev/null
@@ -1,3 +0,0 @@
-%span.log_loading.hide
- %i.fa.fa-spinner.fa-spin
- Loading commit data...
diff --git a/app/views/projects/tree/_submodule_item.html.haml b/app/views/projects/tree/_submodule_item.html.haml
deleted file mode 100644
index e563c8c4036..00000000000
--- a/app/views/projects/tree/_submodule_item.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-%tr.tree-item
- %td.tree-item-file-name
- %i.fa.fa-archive.fa-fw
- = submodule_link(submodule_item, @ref)
- %td
- %td.d-none.d-sm-table-cell
diff --git a/app/views/projects/tree/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml
deleted file mode 100644
index ce0cd95b468..00000000000
--- a/app/views/projects/tree/_tree_item.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-%tr{ class: "tree-item #{tree_hex_class(tree_item)}" }
- %td.tree-item-file-name
- = tree_icon(type, tree_item.mode, tree_item.name)
- - path = flatten_tree(@path, tree_item)
- = link_to project_tree_path(@project, tree_join(@id || @commit.id, path)), class: 'str-truncated', title: path do
- %span= path
- %td.d-none.d-sm-table-cell.tree-commit
- %td.tree-time-ago.text-right
- = render 'projects/tree/spinner'
diff --git a/app/views/projects/tree/_tree_row.html.haml b/app/views/projects/tree/_tree_row.html.haml
index 0a5c6f048f7..8a27ea66523 100644
--- a/app/views/projects/tree/_tree_row.html.haml
+++ b/app/views/projects/tree/_tree_row.html.haml
@@ -1,6 +1,27 @@
-- if tree_row.type == :tree
- = render partial: 'projects/tree/tree_item', object: tree_row, as: 'tree_item', locals: { type: 'folder' }
-- elsif tree_row.type == :blob
- = render partial: 'projects/tree/blob_item', object: tree_row, as: 'blob_item', locals: { type: 'file' }
-- elsif tree_row.type == :commit
- = render partial: 'projects/tree/submodule_item', object: tree_row, as: 'submodule_item'
+- tree_row_name = tree_row.name
+- tree_row_type = tree_row.type
+
+%tr{ class: "tree-item file_#{hexdigest(tree_row_name)}" }
+ %td.tree-item-file-name
+ - if tree_row_type == :tree
+ = tree_icon('folder', tree_row.mode, tree_row.name)
+ - path = flatten_tree(@path, tree_row)
+ %a.str-truncated{ href: fast_project_tree_path(@project, tree_join(@id || @commit.id, path)), title: path }
+ %span= path
+
+ - elsif tree_row_type == :blob
+ = tree_icon('file', tree_row.mode, tree_row_name)
+ %a.str-truncated{ href: fast_project_blob_path(@project, tree_join(@id || @commit.id, tree_row_name)), title: tree_row_name }
+ %span= tree_row_name
+ - if @lfs_blob_ids.include?(tree_row.id)
+ %span.badge.label-lfs.prepend-left-5 LFS
+
+ - elsif tree_row_type == :commit
+ = tree_icon('archive', tree_row.mode, tree_row.name)
+ = submodule_link(tree_row, @ref)
+
+ %td.d-none.d-sm-table-cell.tree-commit
+ %td.tree-time-ago.text-right
+ %span.log_loading.hide
+ %i.fa.fa-spinner.fa-spin
+ Loading commit data...
diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml
index 8b95bdf9747..b4ecd7bbae9 100644
--- a/app/views/search/results/_snippet_blob.html.haml
+++ b/app/views/search/results/_snippet_blob.html.haml
@@ -39,7 +39,7 @@
.blob-content
- snippet_chunks.each do |chunk|
- unless chunk[:data].empty?
- = highlight(snippet.file_name, chunk[:data], repository: nil, plain: snippet.blob.no_highlighting?)
+ = highlight(snippet.file_name, chunk[:data])
- else
.file-content.code
.nothing-here-block Empty file
diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml
index 8d64cb5d698..5073e6ad48f 100644
--- a/app/views/shared/_file_highlight.html.haml
+++ b/app/views/shared/_file_highlight.html.haml
@@ -1,5 +1,3 @@
-- repository = nil unless local_assigns.key?(:repository)
-
.file-content.code.js-syntax-highlight
.line-numbers
- if blob.data.present?
@@ -13,4 +11,6 @@
= link_icon
= i
.blob-content{ data: { blob_id: blob.id } }
- = highlight(blob.path, blob.data, repository: repository, plain: blob.no_highlighting?)
+ %pre.code.highlight
+ %code
+ = blob.present.highlight
diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index dbed4b94d61..973c756f496 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -2,10 +2,19 @@
- group_path = root_url
- group_path << parent.full_path + '/' if parent
-.form-group.row
- = f.label :path, class: 'col-form-label col-sm-2' do
- Group path
- .col-sm-10
+.row
+ .form-group.group-name-holder.col-sm-12
+ = f.label :name, class: 'label-bold' do
+ = _("Group name")
+ = f.text_field :name, placeholder: 'My Awesome Group', class: 'form-control input-lg',
+ required: true,
+ title: _('Please fill in a descriptive name for your group.'),
+ autofocus: true
+
+.row
+ .form-group.col-xs-12.col-sm-8
+ = f.label :path, class: 'label-bold' do
+ = _("Group URL")
.input-group.gl-field-error-anchor
.group-root-path.input-group-prepend.has-tooltip{ title: group_path, :'data-placement' => 'bottom' }
.input-group-text
@@ -13,10 +22,10 @@
- if parent
%strong= parent.full_path + '/'
= f.hidden_field :parent_id
- = f.text_field :path, placeholder: 'open-source', class: 'form-control',
+ = f.text_field :path, placeholder: 'my-awesome-group', class: 'form-control',
autofocus: local_assigns[:autofocus] || false, required: true,
pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS,
- title: 'Please choose a group path with no special characters.',
+ title: _('Please choose a group URL with no special characters.'),
"data-bind-in" => "#{'create_chat_team' if Gitlab.config.mattermost.enabled}"
- if @group.persisted?
@@ -25,23 +34,17 @@
= succeed '.' do
= link_to 'Learn more', help_page_path('user/group/index', anchor: 'changing-a-groups-path'), target: '_blank'
-.form-group.row.group-name-holder
- = f.label :name, class: 'col-form-label col-sm-2' do
- Group name
- .col-sm-10
- = f.text_field :name, class: 'form-control',
- required: true,
- title: 'You can choose a descriptive name different from the path.'
-
- if @group.persisted?
- .form-group.row.group-name-holder
- = f.label :id, class: 'col-form-label col-sm-2' do
- = _("Group ID")
- .col-sm-10
+ .row
+ .form-group.group-name-holder.col-sm-8
+ = f.label :id, class: 'label-bold' do
+ = _("Group ID")
= f.text_field :id, class: 'form-control', readonly: true
-.form-group.row.group-description-holder
- = f.label :description, class: 'col-form-label col-sm-2'
- .col-sm-10
+.row
+ .form-group.group-description-holder.col-sm-8
+ = f.label :description, class: 'label-bold' do
+ = _("Group description")
+ %span (optional)
= f.text_area :description, maxlength: 250,
class: 'form-control js-gfm-input', rows: 4
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index 71f34c0d85b..21ea188d7b3 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -23,28 +23,29 @@
%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
= sprite_icon('pencil')
- %li.inline
- .dropdown
- %button{ type: 'button', class: 'btn btn-transparent js-label-options-dropdown label-action', data: { toggle: 'dropdown' }, aria_label: _('Label actions dropdown') }
- = sprite_icon('ellipsis_v')
- .dropdown-menu.dropdown-open-left
- %ul
- - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group)
- %li
- %button.js-promote-project-label-button.btn.btn-transparent.btn-action{ disabled: true, type: 'button',
- data: { url: promote_project_label_path(label.project, label),
- label_title: label.title,
- label_color: label.color,
- label_text_color: label.text_color,
- group_name: label.project.group.name,
- target: '#promote-label-modal',
- container: 'body',
- toggle: 'modal' } }
- = _('Promote to group label')
- - if can?(current_user, :admin_label, label)
- %li
- %span{ data: { toggle: 'modal', target: "#modal-delete-label-#{label.id}" } }
- %button.text-danger.remove-row{ type: 'button' }= _('Delete')
+ - if can?(current_user, :admin_label, label)
+ %li.inline
+ .dropdown
+ %button{ type: 'button', class: 'btn btn-transparent js-label-options-dropdown label-action', data: { toggle: 'dropdown' }, aria_label: _('Label actions dropdown') }
+ = sprite_icon('ellipsis_v')
+ .dropdown-menu.dropdown-open-left
+ %ul
+ - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group)
+ %li
+ %button.js-promote-project-label-button.btn.btn-transparent.btn-action{ disabled: true, type: 'button',
+ data: { url: promote_project_label_path(label.project, label),
+ label_title: label.title,
+ label_color: label.color,
+ label_text_color: label.text_color,
+ group_name: label.project.group.name,
+ target: '#promote-label-modal',
+ container: 'body',
+ toggle: 'modal' } }
+ = _('Promote to group label')
+ - if can?(current_user, :admin_label, label)
+ %li
+ %span{ data: { toggle: 'modal', target: "#modal-delete-label-#{label.id}" } }
+ %button.text-danger.remove-row{ type: 'button' }= _('Delete')
- if current_user
%li.inline.label-subscription
- if can_subscribe_to_label_in_different_levels?(label)
diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml
index 0d2f6bb77d6..f0d1dd162df 100644
--- a/app/views/shared/boards/_show.html.haml
+++ b/app/views/shared/boards/_show.html.haml
@@ -10,7 +10,7 @@
-# haml-lint:disable InlineJavaScript
%script#js-board-template{ type: "text/x-template" }= render "shared/boards/components/board"
- %script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal
+ %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 }" }
diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml
index 6138914206b..19159684420 100644
--- a/app/views/shared/boards/components/sidebar/_labels.html.haml
+++ b/app/views/shared/boards/components/sidebar/_labels.html.haml
@@ -19,13 +19,13 @@
":value" => "label.id" }
.dropdown
%button.dropdown-menu-toggle.js-label-select.js-multiselect.js-issue-board-sidebar{ type: "button",
- "v-bind:data-selected" => "selectedLabels",
+ ":data-selected" => "selectedLabels",
+ ":data-labels" => "issue.assignableLabelsEndpoint",
data: { toggle: "dropdown",
field_name: "issue[label_names][]",
show_no: "true",
show_any: "true",
project_id: @project&.try(:id),
- labels: labels_filter_path_with_defaults,
namespace_path: @namespace_path,
project_path: @project.try(:path) } }
%span.dropdown-toggle-text
diff --git a/app/views/shared/empty_states/_labels.html.haml b/app/views/shared/empty_states/_labels.html.haml
index b629ceafeb3..bee26cd8312 100644
--- a/app/views/shared/empty_states/_labels.html.haml
+++ b/app/views/shared/empty_states/_labels.html.haml
@@ -6,6 +6,9 @@
.text-content
%h4= _("Labels can be applied to issues and merge requests to categorize them.")
%p= _("You can also star a label to make it a priority label.")
- - if can?(current_user, :admin_label, @project)
- = link_to _('New label'), new_project_label_path(@project), class: 'btn btn-success', title: _('New label'), id: 'new_label_link'
- = link_to _('Generate a default set of labels'), generate_project_labels_path(@project), method: :post, class: 'btn btn-success btn-inverted', title: _('Generate a default set of labels'), id: 'generate_labels_link'
+ .text-center
+ - if can?(current_user, :admin_label, @project)
+ = link_to _('New label'), new_project_label_path(@project), class: 'btn btn-success qa-label-create-new', title: _('New label'), id: 'new_label_link'
+ = link_to _('Generate a default set of labels'), generate_project_labels_path(@project), method: :post, class: 'btn btn-success btn-inverted', title: _('Generate a default set of labels'), id: 'generate_labels_link'
+ - if can?(current_user, :admin_label, @group)
+ = link_to _('New label'), new_group_label_path(@group), class: 'btn btn-success', title: _('New label'), id: 'new_label_link'
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 1d876cc4a5d..95f32bd0180 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -3,6 +3,7 @@
- block_css_class = type != :boards_modal ? 'row-content-block second-block' : ''
- full_path = @project.present? ? @project.full_path : @group.full_path
- user_can_admin_list = board && can?(current_user, :admin_list, board.parent)
+- show_sorting_dropdown = local_assigns.fetch(:show_sorting_dropdown, true)
.issues-filters
.issues-details-filters.filtered-search-block{ class: block_css_class, "v-pre" => type == :boards_modal }
@@ -105,6 +106,14 @@
%span.label-title.js-data-value
{{title}}
#js-dropdown-my-reaction.filtered-search-input-dropdown-menu.dropdown-menu
+ %ul{ data: { dropdown: true } }
+ %li.filter-dropdown-item{ data: { value: 'none' } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('None')
+ %li.filter-dropdown-item{ data: { value: 'any' } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('Any')
+ %li.divider.droplab-item-ignore
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link{ type: 'button' }
@@ -132,5 +141,5 @@
- if @project
#js-add-issues-btn.prepend-left-10{ data: { can_admin_list: can?(current_user, :admin_list, @project) } }
#js-toggle-focus-btn
- - elsif type != :boards_modal
+ - elsif show_sorting_dropdown
= render 'shared/sort_dropdown'
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 2682d92fc56..b4b3f4a6b7e 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -14,6 +14,8 @@
= user_status(user)
%span.cgray= user.to_reference
+ = render_if_exists 'shared/members/ee/sso_badge', member: member
+
- if user == current_user
%span.badge.badge-success.prepend-left-5 It's you
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index 84adbd444c5..bc918430823 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -5,7 +5,7 @@
- note_editable = can?(current_user, :admin_note, note)
- note_counter = local_assigns.fetch(:note_counter, 0)
-%li.timeline-entry{ id: dom_id(note),
+%li.timeline-entry.note-wrapper.outlined{ id: dom_id(note),
class: ["note", "note-row-#{note.id}", ('system-note' if note.system)],
data: { author_id: note.author.id,
editable: note_editable,
diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml
index 9dd1c24fdfa..4c4050c6054 100644
--- a/app/views/shared/notes/_notes_with_form.html.haml
+++ b/app/views/shared/notes/_notes_with_form.html.haml
@@ -7,8 +7,8 @@
= render 'shared/notes/edit_form', project: @project
- if can_create_note?
- %ul.notes.notes-form.timeline
- %li.timeline-entry
+ .notes.notes-form.timeline
+ .timeline-entry.note-form
.timeline-entry-inner
.flash-container.timeline-content
diff --git a/app/views/shared/projects/_search_form.html.haml b/app/views/shared/projects/_search_form.html.haml
index b89194bcc67..3b5c13ed93a 100644
--- a/app/views/shared/projects/_search_form.html.haml
+++ b/app/views/shared/projects/_search_form.html.haml
@@ -21,3 +21,5 @@
- if params[:visibility_level].present?
= hidden_field_tag :visibility_level, params[:visibility_level]
+
+ = render_if_exists 'shared/projects/search_fields'
diff --git a/app/views/sherlock/queries/_general.html.haml b/app/views/sherlock/queries/_general.html.haml
index ddc089b0bd7..52c7bc47ca7 100644
--- a/app/views/sherlock/queries/_general.html.haml
+++ b/app/views/sherlock/queries/_general.html.haml
@@ -36,7 +36,7 @@
%li
.code.js-syntax-highlight.sherlock-code
:preserve
- #{highlight("#{@query.id}.sql", @query.formatted_query)}
+ #{highlight("#{@query.id}.sql", @query.formatted_query, language: 'sql')}
.card
.card-header
diff --git a/app/views/sherlock/transactions/_queries.html.haml b/app/views/sherlock/transactions/_queries.html.haml
index c1ec4b91bb6..5e224f3aa0e 100644
--- a/app/views/sherlock/transactions/_queries.html.haml
+++ b/app/views/sherlock/transactions/_queries.html.haml
@@ -17,7 +17,7 @@
= t('sherlock.milliseconds')
%td
.code.js-syntax-highlight.sherlock-code
- = highlight("#{query.id}.sql", query.formatted_query)
+ = highlight("#{query.id}.sql", query.formatted_query, language: 'sql')
%td
= link_to(t('sherlock.view'),
sherlock_transaction_query_path(@transaction, query),
diff --git a/app/views/users/_overview.html.haml b/app/views/users/_overview.html.haml
index f8b3754840d..cf525f2bb2d 100644
--- a/app/views/users/_overview.html.haml
+++ b/app/views/users/_overview.html.haml
@@ -11,8 +11,8 @@
- if can?(current_user, :read_cross_project)
.activities-block
- .content-block
- %h5.prepend-top-10
+ .border-bottom.prepend-top-16
+ %h5
= s_('UserProfile|Recent contributions')
.overview-content-list{ data: { href: user_path } }
.center.light.loading
@@ -22,7 +22,7 @@
.col-md-12.col-lg-6
.projects-block
- .content-block
+ .border-bottom.prepend-top-16
%h4
= s_('UserProfile|Personal projects')
.overview-content-list{ data: { href: user_projects_path } }
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index f21789de37d..953ab95735b 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -28,6 +28,7 @@
- gcp_cluster:cluster_wait_for_app_installation
- gcp_cluster:wait_for_cluster_creation
- gcp_cluster:cluster_wait_for_ingress_ip_address
+- gcp_cluster:cluster_platform_configure
- github_import_advance_stage
- github_importer:github_import_import_diff_note
@@ -72,6 +73,8 @@
- pipeline_processing:update_head_pipeline_for_merge_request
- pipeline_processing:ci_build_schedule
+- deployment:deployments_success
+
- repository_check:repository_check_clear
- repository_check:repository_check_batch
- repository_check:repository_check_single_repository
diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb
index 51cbbe8882e..61d866b1f02 100644
--- a/app/workers/build_finished_worker.rb
+++ b/app/workers/build_finished_worker.rb
@@ -13,7 +13,7 @@ class BuildFinishedWorker
BuildTraceSectionsWorker.new.perform(build.id)
BuildCoverageWorker.new.perform(build.id)
- # We execute that async as this are two indepentent operations that can be executed after TraceSections and Coverage
+ # We execute that async as this are two independent operations that can be executed after TraceSections and Coverage
BuildHooksWorker.perform_async(build.id)
ArchiveTraceWorker.perform_async(build.id)
end
diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb
index c17608f7378..9a865fea621 100644
--- a/app/workers/build_success_worker.rb
+++ b/app/workers/build_success_worker.rb
@@ -10,13 +10,27 @@ class BuildSuccessWorker
def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build|
create_deployment(build) if build.has_environment?
+ stop_environment(build) if build.stops_environment?
end
end
# rubocop: enable CodeReuse/ActiveRecord
private
+ ##
+ # Deprecated:
+ # As of 11.5, we started creating a deployment record when ci_builds record is created.
+ # Therefore we no longer need to create a deployment, after a build succeeded.
+ # We're leaving this code for the transition period, but we can remove this code in 11.6.
def create_deployment(build)
- CreateDeploymentService.new(build).execute
+ build.create_deployment.try do |deployment|
+ deployment.succeed
+ end
+ end
+
+ ##
+ # TODO: This should be processed in DeploymentSuccessWorker once we started storing `action` value in `deployments` records
+ def stop_environment(build)
+ build.persisted_environment.fire_state_event(:stop)
end
end
diff --git a/app/workers/cluster_platform_configure_worker.rb b/app/workers/cluster_platform_configure_worker.rb
new file mode 100644
index 00000000000..8f3689f0166
--- /dev/null
+++ b/app/workers/cluster_platform_configure_worker.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class ClusterPlatformConfigureWorker
+ include ApplicationWorker
+ include ClusterQueue
+
+ def perform(cluster_id)
+ Clusters::Cluster.find_by_id(cluster_id).try do |cluster|
+ next unless cluster.cluster_project
+
+ kubernetes_namespace = cluster.find_or_initialize_kubernetes_namespace(cluster.cluster_project)
+
+ Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService.new(
+ cluster: cluster,
+ kubernetes_namespace: kubernetes_namespace
+ ).execute
+ end
+
+ rescue ::Kubeclient::HttpError => err
+ Rails.logger.error "Failed to create/update Kubernetes namespace for cluster_id: #{cluster_id} with error: #{err.message}"
+ end
+end
diff --git a/app/workers/cluster_provision_worker.rb b/app/workers/cluster_provision_worker.rb
index 59de7903c1c..3d5894b73ec 100644
--- a/app/workers/cluster_provision_worker.rb
+++ b/app/workers/cluster_provision_worker.rb
@@ -9,6 +9,8 @@ class ClusterProvisionWorker
cluster.provider.try do |provider|
Clusters::Gcp::ProvisionService.new.execute(provider) if cluster.gcp?
end
+
+ ClusterPlatformConfigureWorker.perform_async(cluster.id) if cluster.user?
end
end
end
diff --git a/app/workers/deployments/success_worker.rb b/app/workers/deployments/success_worker.rb
new file mode 100644
index 00000000000..da517f3fb26
--- /dev/null
+++ b/app/workers/deployments/success_worker.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Deployments
+ class SuccessWorker
+ include ApplicationWorker
+
+ queue_namespace :deployment
+
+ def perform(deployment_id)
+ Deployment.find_by_id(deployment_id).try do |deployment|
+ break unless deployment.success?
+
+ UpdateDeploymentService.new(deployment).execute
+ end
+ end
+ end
+end
diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb
index 12706613ac2..bf637f82df2 100644
--- a/app/workers/email_receiver_worker.rb
+++ b/app/workers/email_receiver_worker.rb
@@ -40,6 +40,8 @@ class EmailReceiverWorker
"You are not allowed to perform this action. If you believe this is in error, contact a staff member."
when Gitlab::Email::NoteableNotFoundError
"The thread you are replying to no longer exists, perhaps it was deleted? If you believe this is in error, contact a staff member."
+ when Gitlab::Email::InvalidAttachment
+ error.message
when Gitlab::Email::InvalidRecordError
can_retry = true
error.message
diff --git a/app/workers/gitlab/github_import/advance_stage_worker.rb b/app/workers/gitlab/github_import/advance_stage_worker.rb
index cd2ceb8dcdf..2b49860025a 100644
--- a/app/workers/gitlab/github_import/advance_stage_worker.rb
+++ b/app/workers/gitlab/github_import/advance_stage_worker.rb
@@ -14,7 +14,7 @@ module Gitlab
INTERVAL = 30.seconds.to_i
# The number of seconds to wait (while blocking the thread) before
- # continueing to the next waiter.
+ # continuing to the next waiter.
BLOCKING_WAIT_TIME = 5
# The known importer stages and their corresponding Sidekiq workers.
diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb
index 7b167c95c29..29a7f8e691a 100644
--- a/app/workers/process_commit_worker.rb
+++ b/app/workers/process_commit_worker.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# Worker for processing individiual commit messages pushed to a repository.
+# Worker for processing individual commit messages pushed to a repository.
#
# Jobs for this worker are scheduled for every commit that is being pushed. As a
# result of this the workload of this worker should be kept to a bare minimum.
diff --git a/app/workers/stuck_import_jobs_worker.rb b/app/workers/stuck_import_jobs_worker.rb
index de92f3eca6a..667a4121131 100644
--- a/app/workers/stuck_import_jobs_worker.rb
+++ b/app/workers/stuck_import_jobs_worker.rb
@@ -7,60 +7,58 @@ class StuckImportJobsWorker
IMPORT_JOBS_EXPIRATION = 15.hours.to_i
def perform
- projects_without_jid_count = mark_projects_without_jid_as_failed!
- projects_with_jid_count = mark_projects_with_jid_as_failed!
+ import_state_without_jid_count = mark_import_states_without_jid_as_failed!
+ import_state_with_jid_count = mark_import_states_with_jid_as_failed!
Gitlab::Metrics.add_event(:stuck_import_jobs,
- projects_without_jid_count: projects_without_jid_count,
- projects_with_jid_count: projects_with_jid_count)
+ projects_without_jid_count: import_state_without_jid_count,
+ projects_with_jid_count: import_state_with_jid_count)
end
private
- def mark_projects_without_jid_as_failed!
- enqueued_projects_without_jid.each do |project|
- project.mark_import_as_failed(error_message)
+ def mark_import_states_without_jid_as_failed!
+ enqueued_import_states_without_jid.each do |import_state|
+ import_state.mark_as_failed(error_message)
end.count
end
# rubocop: disable CodeReuse/ActiveRecord
- def mark_projects_with_jid_as_failed!
- # TODO: Rollback this change to use SQL through #pluck
- jids_and_ids = enqueued_projects_with_jid.map { |project| [project.import_jid, project.id] }.to_h
+ def mark_import_states_with_jid_as_failed!
+ jids_and_ids = enqueued_import_states_with_jid.pluck(:jid, :id).to_h
# Find the jobs that aren't currently running or that exceeded the threshold.
completed_jids = Gitlab::SidekiqStatus.completed_jids(jids_and_ids.keys)
return unless completed_jids.any?
- completed_project_ids = jids_and_ids.values_at(*completed_jids)
+ completed_import_state_ids = jids_and_ids.values_at(*completed_jids)
- # We select the projects again, because they may have transitioned from
+ # We select the import states again, because they may have transitioned from
# scheduled/started to finished/failed while we were looking up their Sidekiq status.
- completed_projects = enqueued_projects_with_jid.where(id: completed_project_ids)
+ completed_import_states = enqueued_import_states_with_jid.where(id: completed_import_state_ids)
- Rails.logger.info("Marked stuck import jobs as failed. JIDs: #{completed_projects.map(&:import_jid).join(', ')}")
+ completed_import_state_jids = completed_import_states.map { |import_state| import_state.jid }.join(', ')
+ Rails.logger.info("Marked stuck import jobs as failed. JIDs: #{completed_import_state_jids}")
- completed_projects.each do |project|
- project.mark_import_as_failed(error_message)
+ completed_import_states.each do |import_state|
+ import_state.mark_as_failed(error_message)
end.count
end
# rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
- def enqueued_projects
- Project.joins_import_state.where("(import_state.status = 'scheduled' OR import_state.status = 'started') OR (projects.import_status = 'scheduled' OR projects.import_status = 'started')")
+ def enqueued_import_states
+ ProjectImportState.with_status([:scheduled, :started])
end
- # rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
- def enqueued_projects_with_jid
- enqueued_projects.where.not("import_state.jid IS NULL AND projects.import_jid IS NULL")
+ def enqueued_import_states_with_jid
+ enqueued_import_states.where.not(jid: nil)
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
- def enqueued_projects_without_jid
- enqueued_projects.where("import_state.jid IS NULL AND projects.import_jid IS NULL")
+ def enqueued_import_states_without_jid
+ enqueued_import_states.where(jid: nil)
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/bin/rails b/bin/rails
index 228f812ccaf..d21b64b3007 100755
--- a/bin/rails
+++ b/bin/rails
@@ -1,7 +1,7 @@
#!/usr/bin/env ruby
# Remove this block when upgraded to rails 5.0.
-unless %w[1 true].include?(ENV["RAILS5"])
+if %w[0 false].include?(ENV["RAILS5"])
begin
load File.expand_path('../spring', __FILE__)
rescue LoadError => e
diff --git a/bin/rake b/bin/rake
index b52a8321f1a..1362d51e3d6 100755
--- a/bin/rake
+++ b/bin/rake
@@ -1,7 +1,7 @@
#!/usr/bin/env ruby
# Remove this block when upgraded to rails 5.0.
-unless %w[1 true].include?(ENV["RAILS5"])
+if %w[0 false].include?(ENV["RAILS5"])
begin
load File.expand_path('../spring', __FILE__)
rescue LoadError => e
diff --git a/bin/rspec b/bin/rspec
index 26583242051..b0770e30a70 100755
--- a/bin/rspec
+++ b/bin/rspec
@@ -2,7 +2,7 @@
# Remove these two lines below when upgraded to rails 5.0.
# Allow run `rspec` command as `RAILS5=1 rspec ...` instead of `BUNDLE_GEMFILE=Gemfile.rails5 rspec ...`
-gemfile = %w[1 true].include?(ENV["RAILS5"]) ? "Gemfile.rails5" : "Gemfile"
+gemfile = %w[0 false].include?(ENV["RAILS5"]) ? "Gemfile.rails4" : "Gemfile"
ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../#{gemfile}", __dir__)
begin
diff --git a/bin/setup b/bin/setup
index ec1ebe02950..34bb667087a 100755
--- a/bin/setup
+++ b/bin/setup
@@ -1,7 +1,7 @@
#!/usr/bin/env ruby
def rails5?
- %w[1 true].include?(ENV["RAILS5"])
+ !%w[0 false].include?(ENV["RAILS5"])
end
require "pathname"
diff --git a/changelogs/unreleased/18933-render-index-as-readme.yml b/changelogs/unreleased/18933-render-index-as-readme.yml
new file mode 100644
index 00000000000..44acc2c719a
--- /dev/null
+++ b/changelogs/unreleased/18933-render-index-as-readme.yml
@@ -0,0 +1,5 @@
+---
+title: Make index.* render like README.* when it's present in a repository
+merge_request: 22639
+author: Jakub Jirutka
+type: added
diff --git a/changelogs/unreleased/21480-parallel-job-keyword-mvc.yml b/changelogs/unreleased/21480-parallel-job-keyword-mvc.yml
new file mode 100644
index 00000000000..7ac2410b18c
--- /dev/null
+++ b/changelogs/unreleased/21480-parallel-job-keyword-mvc.yml
@@ -0,0 +1,5 @@
+---
+title: Implement parallel job keyword.
+merge_request: 22631
+author:
+type: added
diff --git a/changelogs/unreleased/22717-single-letter-identifier-external-issue-tracker.yml b/changelogs/unreleased/22717-single-letter-identifier-external-issue-tracker.yml
new file mode 100644
index 00000000000..3f7a0d9204e
--- /dev/null
+++ b/changelogs/unreleased/22717-single-letter-identifier-external-issue-tracker.yml
@@ -0,0 +1,5 @@
+---
+title: "Allowing issues with single letter identifiers to be linked to external issue tracker (f.ex T-123)"
+merge_request: 22717
+author: Dídac Rodríguez Arbonès
+type: changed \ No newline at end of file
diff --git a/changelogs/unreleased/25140-disable-stop-button.yml b/changelogs/unreleased/25140-disable-stop-button.yml
new file mode 100644
index 00000000000..a6ef52c3155
--- /dev/null
+++ b/changelogs/unreleased/25140-disable-stop-button.yml
@@ -0,0 +1,5 @@
+---
+title: Disables stop environment button while the deploy is in progress
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/28249-add-pagination.yml b/changelogs/unreleased/28249-add-pagination.yml
new file mode 100644
index 00000000000..df15094405a
--- /dev/null
+++ b/changelogs/unreleased/28249-add-pagination.yml
@@ -0,0 +1,5 @@
+---
+title: Adds pagination to pipelines table in merge request page
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/34758-group-cluster-controller.yml b/changelogs/unreleased/34758-group-cluster-controller.yml
new file mode 100644
index 00000000000..88c4c872714
--- /dev/null
+++ b/changelogs/unreleased/34758-group-cluster-controller.yml
@@ -0,0 +1,5 @@
+---
+title: Add ability to create group level clusters and install gitlab managed applications
+merge_request: 22450
+author:
+type: added
diff --git a/changelogs/unreleased/41545-gitlab-merge-request-status-could-not-connect-to-the-ci-server-please-check-your-settings-and-try-again.yml b/changelogs/unreleased/41545-gitlab-merge-request-status-could-not-connect-to-the-ci-server-please-check-your-settings-and-try-again.yml
new file mode 100644
index 00000000000..103419c1185
--- /dev/null
+++ b/changelogs/unreleased/41545-gitlab-merge-request-status-could-not-connect-to-the-ci-server-please-check-your-settings-and-try-again.yml
@@ -0,0 +1,5 @@
+---
+title: Reword error message for internal CI unknown pipeline status
+merge_request: 22474
+author:
+type: changed
diff --git a/changelogs/unreleased/43521-keep-personal-emails-private.yml b/changelogs/unreleased/43521-keep-personal-emails-private.yml
new file mode 100644
index 00000000000..0f0bede6482
--- /dev/null
+++ b/changelogs/unreleased/43521-keep-personal-emails-private.yml
@@ -0,0 +1,5 @@
+---
+title: Adds option to override commit email with a noreply private email
+merge_request: 22560
+author:
+type: added
diff --git a/changelogs/unreleased/44012-filter-reactions-none-any.yml b/changelogs/unreleased/44012-filter-reactions-none-any.yml
new file mode 100644
index 00000000000..5d685010f8a
--- /dev/null
+++ b/changelogs/unreleased/44012-filter-reactions-none-any.yml
@@ -0,0 +1,5 @@
+---
+title: Add None / Any options to reactions filter
+merge_request: 22638
+author: Heinrich Lee Yu
+type: added
diff --git a/changelogs/unreleased/47008-issue-board-card-design.yml b/changelogs/unreleased/47008-issue-board-card-design.yml
new file mode 100644
index 00000000000..39238687943
--- /dev/null
+++ b/changelogs/unreleased/47008-issue-board-card-design.yml
@@ -0,0 +1,5 @@
+---
+title: Issue board card design
+merge_request: 21229
+author:
+type: changed
diff --git a/changelogs/unreleased/48475-gitlab-pages-settings-regressions.yml b/changelogs/unreleased/48475-gitlab-pages-settings-regressions.yml
new file mode 100644
index 00000000000..f543730a57d
--- /dev/null
+++ b/changelogs/unreleased/48475-gitlab-pages-settings-regressions.yml
@@ -0,0 +1,5 @@
+---
+title: Fixing regression issues on pages settings and details
+merge_request: 22821
+author:
+type: fixed
diff --git a/changelogs/unreleased/49403-redesign-activity-feed.yml b/changelogs/unreleased/49403-redesign-activity-feed.yml
new file mode 100644
index 00000000000..cec53a3ef5a
--- /dev/null
+++ b/changelogs/unreleased/49403-redesign-activity-feed.yml
@@ -0,0 +1,4 @@
+title: Redesign activity feed
+merge_request: 22217
+author:
+type: other
diff --git a/changelogs/unreleased/50962-create-new-group-rename-form-fields-and-update-ui.yml b/changelogs/unreleased/50962-create-new-group-rename-form-fields-and-update-ui.yml
new file mode 100644
index 00000000000..db374e10c36
--- /dev/null
+++ b/changelogs/unreleased/50962-create-new-group-rename-form-fields-and-update-ui.yml
@@ -0,0 +1,5 @@
+---
+title: 'Create new group: Rename form fields and update UI'
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/51259-ci-cd-gitlab-ui-1.yml b/changelogs/unreleased/51259-ci-cd-gitlab-ui-1.yml
new file mode 100644
index 00000000000..1d761d6299c
--- /dev/null
+++ b/changelogs/unreleased/51259-ci-cd-gitlab-ui-1.yml
@@ -0,0 +1,5 @@
+---
+title: Uses new gitlab-ui components in Jobs and Pipelines components
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/51259-ci-cd-gitlab-ui.yml b/changelogs/unreleased/51259-ci-cd-gitlab-ui.yml
new file mode 100644
index 00000000000..a15f1c033b3
--- /dev/null
+++ b/changelogs/unreleased/51259-ci-cd-gitlab-ui.yml
@@ -0,0 +1,5 @@
+---
+title: Uses gitlab-ui components in jobs components
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/51259-ci-cd-tooltips.yml b/changelogs/unreleased/51259-ci-cd-tooltips.yml
new file mode 100644
index 00000000000..fc0010dbeba
--- /dev/null
+++ b/changelogs/unreleased/51259-ci-cd-tooltips.yml
@@ -0,0 +1,6 @@
+---
+title: Replaces tooltip directive with the new gl-tooltip directive for consistency
+ in some ci/cd code
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/51620-cannot-add-label-to-issue-from-board.yml b/changelogs/unreleased/51620-cannot-add-label-to-issue-from-board.yml
new file mode 100644
index 00000000000..9e99779d352
--- /dev/null
+++ b/changelogs/unreleased/51620-cannot-add-label-to-issue-from-board.yml
@@ -0,0 +1,4 @@
+title: Make Issue Board sidebar show project-specific labels based on selected Issue
+merge_request: 22475
+author:
+type: fixed
diff --git a/changelogs/unreleased/51716-add-kubernetes-namespace-background-migration.yml b/changelogs/unreleased/51716-add-kubernetes-namespace-background-migration.yml
new file mode 100644
index 00000000000..89a91e8deaf
--- /dev/null
+++ b/changelogs/unreleased/51716-add-kubernetes-namespace-background-migration.yml
@@ -0,0 +1,5 @@
+---
+title: Add background migration to populate Kubernetes namespaces
+merge_request: 22433
+author:
+type: added
diff --git a/changelogs/unreleased/51716-create-kube-namespace.yml b/changelogs/unreleased/51716-create-kube-namespace.yml
new file mode 100644
index 00000000000..851e19c0a38
--- /dev/null
+++ b/changelogs/unreleased/51716-create-kube-namespace.yml
@@ -0,0 +1,5 @@
+---
+title: Extend RBAC by having a service account restricted to project's namespace
+merge_request: 22011
+author:
+type: other
diff --git a/changelogs/unreleased/52300-pool-repositories.yml b/changelogs/unreleased/52300-pool-repositories.yml
new file mode 100644
index 00000000000..5435f3aa21f
--- /dev/null
+++ b/changelogs/unreleased/52300-pool-repositories.yml
@@ -0,0 +1,5 @@
+---
+title: Start tracking shards and pool repositories in the database
+merge_request: 22482
+author:
+type: other
diff --git a/changelogs/unreleased/52382-filter-milestone-api-none-any.yml b/changelogs/unreleased/52382-filter-milestone-api-none-any.yml
new file mode 100644
index 00000000000..a7559a25645
--- /dev/null
+++ b/changelogs/unreleased/52382-filter-milestone-api-none-any.yml
@@ -0,0 +1,5 @@
+---
+title: Standardize milestones filter in APIs to None / Any
+merge_request: 22637
+author: Heinrich Lee Yu
+type: changed
diff --git a/changelogs/unreleased/52548-links-in-tabs-of-the-labels-index-pages-ends-with-html.yml b/changelogs/unreleased/52548-links-in-tabs-of-the-labels-index-pages-ends-with-html.yml
new file mode 100644
index 00000000000..052ef70c41a
--- /dev/null
+++ b/changelogs/unreleased/52548-links-in-tabs-of-the-labels-index-pages-ends-with-html.yml
@@ -0,0 +1,5 @@
+---
+title: Fix bug when links in tabs of the labels index pages ends with .html
+merge_request: 22716
+author:
+type: fixed
diff --git a/changelogs/unreleased/52767-more-chaos-for-gitlab.yml b/changelogs/unreleased/52767-more-chaos-for-gitlab.yml
new file mode 100644
index 00000000000..067777cb7fa
--- /dev/null
+++ b/changelogs/unreleased/52767-more-chaos-for-gitlab.yml
@@ -0,0 +1,5 @@
+---
+title: Add endpoints for simulating certain failure modes in the application
+merge_request: 22746
+author:
+type: other
diff --git a/changelogs/unreleased/52771-ldap-users-can-t-choose-private-or-internal-when-creating-a-new-group.yml b/changelogs/unreleased/52771-ldap-users-can-t-choose-private-or-internal-when-creating-a-new-group.yml
new file mode 100644
index 00000000000..a05ef75b6a6
--- /dev/null
+++ b/changelogs/unreleased/52771-ldap-users-can-t-choose-private-or-internal-when-creating-a-new-group.yml
@@ -0,0 +1,5 @@
+---
+title: Fix bug stopping non-admin users from changing visibility level on group creation
+merge_request: 22468
+author:
+type: fixed
diff --git a/changelogs/unreleased/52925-scheduled-pipelines-ui-problems.yml b/changelogs/unreleased/52925-scheduled-pipelines-ui-problems.yml
new file mode 100644
index 00000000000..792b24d75ac
--- /dev/null
+++ b/changelogs/unreleased/52925-scheduled-pipelines-ui-problems.yml
@@ -0,0 +1,5 @@
+---
+title: Fixing styling issues on the scheduled pipelines page
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/52940-fix-internal-email-pattern-not-respected.yml b/changelogs/unreleased/52940-fix-internal-email-pattern-not-respected.yml
new file mode 100644
index 00000000000..98e15a5cc0a
--- /dev/null
+++ b/changelogs/unreleased/52940-fix-internal-email-pattern-not-respected.yml
@@ -0,0 +1,5 @@
+---
+title: Fix a bug where internal email pattern wasn't respected
+merge_request: 22516
+author:
+type: fixed
diff --git a/changelogs/unreleased/53052-mg-fix-broken-ie11.yml b/changelogs/unreleased/53052-mg-fix-broken-ie11.yml
new file mode 100644
index 00000000000..c616efffa6b
--- /dev/null
+++ b/changelogs/unreleased/53052-mg-fix-broken-ie11.yml
@@ -0,0 +1,5 @@
+---
+title: Fix incompatibility with IE11 due to non-transpiled gitlab-ui components
+merge_request: 22695
+author:
+type: fixed
diff --git a/changelogs/unreleased/53070-fix-enable-usage-ping-link.yml b/changelogs/unreleased/53070-fix-enable-usage-ping-link.yml
deleted file mode 100644
index 605d3679159..00000000000
--- a/changelogs/unreleased/53070-fix-enable-usage-ping-link.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: "fix link to enable usage ping from convdev index"
-merge_request: 22545
-author: Anand Capur
-type: fixed
diff --git a/changelogs/unreleased/53227-empty-list.yml b/changelogs/unreleased/53227-empty-list.yml
new file mode 100644
index 00000000000..8b222145299
--- /dev/null
+++ b/changelogs/unreleased/53227-empty-list.yml
@@ -0,0 +1,6 @@
+---
+title: Only renders dropdown for review app changes when we have a list of files to
+ show. Otherwise will render the regular review app button
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/53230-remove_personal_access_tokens_finder_find_by_method.yml b/changelogs/unreleased/53230-remove_personal_access_tokens_finder_find_by_method.yml
new file mode 100644
index 00000000000..d4d78a2fd06
--- /dev/null
+++ b/changelogs/unreleased/53230-remove_personal_access_tokens_finder_find_by_method.yml
@@ -0,0 +1,5 @@
+---
+title: Remove PersonalAccessTokensFinder#find_by method
+merge_request: 22617
+author:
+type: fixed
diff --git a/changelogs/unreleased/53289-update-haml_lint-to-0-28-0.yml b/changelogs/unreleased/53289-update-haml_lint-to-0-28-0.yml
new file mode 100644
index 00000000000..9a16666c416
--- /dev/null
+++ b/changelogs/unreleased/53289-update-haml_lint-to-0-28-0.yml
@@ -0,0 +1,5 @@
+---
+title: Update haml_lint to 0.28.0
+merge_request: 22660
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/53291-update-ffaker-to-2-10-0.yml b/changelogs/unreleased/53291-update-ffaker-to-2-10-0.yml
new file mode 100644
index 00000000000..a1b95df5e32
--- /dev/null
+++ b/changelogs/unreleased/53291-update-ffaker-to-2-10-0.yml
@@ -0,0 +1,5 @@
+---
+title: Update ffaker to 2.10.0
+merge_request: 22661
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/53362-allow-concurrency-in-puma.yml b/changelogs/unreleased/53362-allow-concurrency-in-puma.yml
new file mode 100644
index 00000000000..5fbda0161c1
--- /dev/null
+++ b/changelogs/unreleased/53362-allow-concurrency-in-puma.yml
@@ -0,0 +1,5 @@
+---
+title: Allow Rails concurrency when running in Puma
+merge_request: 22751
+author:
+type: performance
diff --git a/changelogs/unreleased/53450-wrong-value-for-kubernetes_version-variable.yml b/changelogs/unreleased/53450-wrong-value-for-kubernetes_version-variable.yml
new file mode 100644
index 00000000000..cd9300ca2d1
--- /dev/null
+++ b/changelogs/unreleased/53450-wrong-value-for-kubernetes_version-variable.yml
@@ -0,0 +1,5 @@
+---
+title: Bump KUBERNETES_VERSION for Auto DevOps to latest 1.10 series
+merge_request: 22757
+author:
+type: other
diff --git a/changelogs/unreleased/53533-fix-broken-link.yml b/changelogs/unreleased/53533-fix-broken-link.yml
new file mode 100644
index 00000000000..6d55c75d82e
--- /dev/null
+++ b/changelogs/unreleased/53533-fix-broken-link.yml
@@ -0,0 +1,5 @@
+---
+title: Render unescaped link for failed pipeline status
+merge_request: 22807
+author:
+type: fixed
diff --git a/changelogs/unreleased/53535-sticky-archived.yml b/changelogs/unreleased/53535-sticky-archived.yml
new file mode 100644
index 00000000000..8d452d84871
--- /dev/null
+++ b/changelogs/unreleased/53535-sticky-archived.yml
@@ -0,0 +1,5 @@
+---
+title: Renders warning info when job is archieved
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/53626-update-config-map-on-install-retry.yml b/changelogs/unreleased/53626-update-config-map-on-install-retry.yml
new file mode 100644
index 00000000000..38e79c06c89
--- /dev/null
+++ b/changelogs/unreleased/53626-update-config-map-on-install-retry.yml
@@ -0,0 +1,5 @@
+---
+title: Update config map for gitlab managed application if already present on install
+merge_request: 22969
+author:
+type: other
diff --git a/changelogs/unreleased/53636-fix-rendering-of-any-user-filter.yml b/changelogs/unreleased/53636-fix-rendering-of-any-user-filter.yml
new file mode 100644
index 00000000000..a59a276a334
--- /dev/null
+++ b/changelogs/unreleased/53636-fix-rendering-of-any-user-filter.yml
@@ -0,0 +1,5 @@
+---
+title: Fix rendering of filter bar tokens for special values
+merge_request: 22865
+author: Heinrich Lee Yu
+type: fixed
diff --git a/changelogs/unreleased/53640-follow-up-from-resolve-redesign-activity-feed.yml b/changelogs/unreleased/53640-follow-up-from-resolve-redesign-activity-feed.yml
new file mode 100644
index 00000000000..66301329c52
--- /dev/null
+++ b/changelogs/unreleased/53640-follow-up-from-resolve-redesign-activity-feed.yml
@@ -0,0 +1,4 @@
+title: Adds new icon size to Vue icon component
+merge_request: 22899
+author:
+type: other
diff --git a/changelogs/unreleased/53816-empty-label-menu-if-not-logged-in.yml b/changelogs/unreleased/53816-empty-label-menu-if-not-logged-in.yml
new file mode 100644
index 00000000000..a9ca56303eb
--- /dev/null
+++ b/changelogs/unreleased/53816-empty-label-menu-if-not-logged-in.yml
@@ -0,0 +1,5 @@
+---
+title: Removes promote to group label for anonymous user
+merge_request: 23042
+author: Jacopo Beschi @jacopo-beschi
+type: fixed
diff --git a/changelogs/unreleased/53879-kube-token-nil.yml b/changelogs/unreleased/53879-kube-token-nil.yml
new file mode 100644
index 00000000000..61a0db15d84
--- /dev/null
+++ b/changelogs/unreleased/53879-kube-token-nil.yml
@@ -0,0 +1,5 @@
+---
+title: Fix deployment jobs using nil KUBE_TOKEN due to migration issue
+merge_request: 23009
+author:
+type: fixed
diff --git a/changelogs/unreleased/53888-missing-favicon.yml b/changelogs/unreleased/53888-missing-favicon.yml
new file mode 100644
index 00000000000..ba6f26c6b9f
--- /dev/null
+++ b/changelogs/unreleased/53888-missing-favicon.yml
@@ -0,0 +1,5 @@
+---
+title: Adds CI favicon back to jobs page
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/53972-fix-fill-shards.yml b/changelogs/unreleased/53972-fix-fill-shards.yml
new file mode 100644
index 00000000000..ca94d6cc589
--- /dev/null
+++ b/changelogs/unreleased/53972-fix-fill-shards.yml
@@ -0,0 +1,5 @@
+---
+title: Fix a race condition intermittently breaking GitLab startup
+merge_request: 23028
+author:
+type: fixed
diff --git a/changelogs/unreleased/54002-activity-feed-missing-padding-in-event-note-when-a-branch-is-deleted.yml b/changelogs/unreleased/54002-activity-feed-missing-padding-in-event-note-when-a-branch-is-deleted.yml
new file mode 100644
index 00000000000..9f4f104a12c
--- /dev/null
+++ b/changelogs/unreleased/54002-activity-feed-missing-padding-in-event-note-when-a-branch-is-deleted.yml
@@ -0,0 +1,5 @@
+---
+title: Adds margin after a deleted branch name in the activity feed.
+merge_request: 23038
+author:
+type: fixed
diff --git a/changelogs/unreleased/6500-fix-misaligned-approvers-dropdown.yml b/changelogs/unreleased/6500-fix-misaligned-approvers-dropdown.yml
new file mode 100644
index 00000000000..3e87c5875c6
--- /dev/null
+++ b/changelogs/unreleased/6500-fix-misaligned-approvers-dropdown.yml
@@ -0,0 +1,5 @@
+---
+title: Fix misaligned approvers dropdown
+merge_request: 22832
+author:
+type: fixed
diff --git a/changelogs/unreleased/7737-ci-pipeline-view-slowed-down-massivly-if-security-tabs-has-many-entries-ee.yml b/changelogs/unreleased/7737-ci-pipeline-view-slowed-down-massivly-if-security-tabs-has-many-entries-ee.yml
new file mode 100644
index 00000000000..aaae8feb220
--- /dev/null
+++ b/changelogs/unreleased/7737-ci-pipeline-view-slowed-down-massivly-if-security-tabs-has-many-entries-ee.yml
@@ -0,0 +1,5 @@
+---
+title: Improve performance of rendering large reports
+merge_request: 22835
+author:
+type: performance
diff --git a/changelogs/unreleased/ab-45608-stuck-mr-query.yml b/changelogs/unreleased/ab-45608-stuck-mr-query.yml
new file mode 100644
index 00000000000..3b64534e480
--- /dev/null
+++ b/changelogs/unreleased/ab-45608-stuck-mr-query.yml
@@ -0,0 +1,5 @@
+---
+title: Add index to find stuck merge requests.
+merge_request: 22749
+author:
+type: performance
diff --git a/changelogs/unreleased/add-action-to-deployment.yml b/changelogs/unreleased/add-action-to-deployment.yml
new file mode 100644
index 00000000000..4629f762ae8
--- /dev/null
+++ b/changelogs/unreleased/add-action-to-deployment.yml
@@ -0,0 +1,5 @@
+---
+title: Fix environment status in merge request widget
+merge_request: 22799
+author:
+type: changed
diff --git a/changelogs/unreleased/add-scheduled-flag-to-job-entity.yml b/changelogs/unreleased/add-scheduled-flag-to-job-entity.yml
new file mode 100644
index 00000000000..a80b5a931b9
--- /dev/null
+++ b/changelogs/unreleased/add-scheduled-flag-to-job-entity.yml
@@ -0,0 +1,5 @@
+---
+title: Add scheduled flag to job entity
+merge_request: 22710
+author:
+type: other
diff --git a/changelogs/unreleased/ashmckenzie-hmac-token-decode-and-tests.yml b/changelogs/unreleased/ashmckenzie-hmac-token-decode-and-tests.yml
new file mode 100644
index 00000000000..d15c5654d99
--- /dev/null
+++ b/changelogs/unreleased/ashmckenzie-hmac-token-decode-and-tests.yml
@@ -0,0 +1,5 @@
+---
+title: Relocate JSONWebToken::HMACToken from EE
+merge_request: 22906
+author:
+type: changed
diff --git a/changelogs/unreleased/auto_devops_kubernetes_active.yml b/changelogs/unreleased/auto_devops_kubernetes_active.yml
new file mode 100644
index 00000000000..310d37128c9
--- /dev/null
+++ b/changelogs/unreleased/auto_devops_kubernetes_active.yml
@@ -0,0 +1,5 @@
+---
+title: Switch kubernetes:active with checking in Auto-DevOps.gitlab-ci.yml
+merge_request: 22929
+author:
+type: fixed
diff --git a/changelogs/unreleased/blackst0ne-add-discord-service.yml b/changelogs/unreleased/blackst0ne-add-discord-service.yml
new file mode 100644
index 00000000000..85dedf6d81f
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-add-discord-service.yml
@@ -0,0 +1,5 @@
+---
+title: Add Discord integration
+merge_request: 22684
+author: "@blackst0ne"
+type: added
diff --git a/changelogs/unreleased/blackst0ne-update-push-new-merge-request-url.yml b/changelogs/unreleased/blackst0ne-update-push-new-merge-request-url.yml
deleted file mode 100644
index b89ba754952..00000000000
--- a/changelogs/unreleased/blackst0ne-update-push-new-merge-request-url.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make new merge request URL more friendly when pushing code
-merge_request: 22526
-author: "@blackst0ne"
-type: changed
diff --git a/changelogs/unreleased/bvl-patches-via-mail.yml b/changelogs/unreleased/bvl-patches-via-mail.yml
new file mode 100644
index 00000000000..6fd9e6a956c
--- /dev/null
+++ b/changelogs/unreleased/bvl-patches-via-mail.yml
@@ -0,0 +1,5 @@
+---
+title: Allow adding patches when creating a merge request via email
+merge_request: 22723
+author: Serdar Dogruyol
+type: added
diff --git a/changelogs/unreleased/ccr-51052_keep_labels_on_issue.yml b/changelogs/unreleased/ccr-51052_keep_labels_on_issue.yml
new file mode 100644
index 00000000000..7ef857d38ed
--- /dev/null
+++ b/changelogs/unreleased/ccr-51052_keep_labels_on_issue.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed label removal from issue
+merge_request: 22762
+author:
+type: fixed
diff --git a/changelogs/unreleased/ccr-51520_change_milestone_email.yml b/changelogs/unreleased/ccr-51520_change_milestone_email.yml
new file mode 100644
index 00000000000..ce4beba2c5f
--- /dev/null
+++ b/changelogs/unreleased/ccr-51520_change_milestone_email.yml
@@ -0,0 +1,5 @@
+---
+title: Add email for milestone change
+merge_request: 22279
+author:
+type: added
diff --git a/changelogs/unreleased/diff-expand-all-button.yml b/changelogs/unreleased/diff-expand-all-button.yml
new file mode 100644
index 00000000000..77600e726d5
--- /dev/null
+++ b/changelogs/unreleased/diff-expand-all-button.yml
@@ -0,0 +1,5 @@
+---
+title: Show expand all diffs button when a single diff file is collapsed
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/disallow-retry-of-old-builds.yml b/changelogs/unreleased/disallow-retry-of-old-builds.yml
new file mode 100644
index 00000000000..03992fc0213
--- /dev/null
+++ b/changelogs/unreleased/disallow-retry-of-old-builds.yml
@@ -0,0 +1,5 @@
+---
+title: Soft-archive old jobs
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/discussion-perf-improvement.yml b/changelogs/unreleased/discussion-perf-improvement.yml
new file mode 100644
index 00000000000..defff8a55f5
--- /dev/null
+++ b/changelogs/unreleased/discussion-perf-improvement.yml
@@ -0,0 +1,5 @@
+---
+title: Improve initial discussion rendering performance
+merge_request: 22607
+author:
+type: changed
diff --git a/changelogs/unreleased/dm-api-merge-requests-index-merged-at.yml b/changelogs/unreleased/dm-api-merge-requests-index-merged-at.yml
new file mode 100644
index 00000000000..8e02a9019df
--- /dev/null
+++ b/changelogs/unreleased/dm-api-merge-requests-index-merged-at.yml
@@ -0,0 +1,5 @@
+---
+title: Expose {closed,merged}_{at,by} in merge requests API index
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/dm-commit-email-select-options.yml b/changelogs/unreleased/dm-commit-email-select-options.yml
new file mode 100644
index 00000000000..90d5c8cf0c6
--- /dev/null
+++ b/changelogs/unreleased/dm-commit-email-select-options.yml
@@ -0,0 +1,5 @@
+---
+title: Fix bug causing not all emails to show up in commit email selectbox
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/drop-gcp-cluster-table.yml b/changelogs/unreleased/drop-gcp-cluster-table.yml
new file mode 100644
index 00000000000..15964ec2eaf
--- /dev/null
+++ b/changelogs/unreleased/drop-gcp-cluster-table.yml
@@ -0,0 +1,5 @@
+---
+title: Drop gcp_clusters table
+merge_request: 22713
+author:
+type: other
diff --git a/changelogs/unreleased/fast_project_blob_path.yml b/changelogs/unreleased/fast_project_blob_path.yml
new file mode 100644
index 00000000000..b56c9d9cf59
--- /dev/null
+++ b/changelogs/unreleased/fast_project_blob_path.yml
@@ -0,0 +1,5 @@
+---
+title: Improve performance of tree rendering in repositories with lots of items
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/fix-deployment-metrics-in-mr-widget.yml b/changelogs/unreleased/fix-deployment-metrics-in-mr-widget.yml
new file mode 100644
index 00000000000..5427ead3d1b
--- /dev/null
+++ b/changelogs/unreleased/fix-deployment-metrics-in-mr-widget.yml
@@ -0,0 +1,6 @@
+---
+title: Avoid returning deployment metrics url to MR widget when the deployment is
+ not successful
+merge_request: 23010
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-error-handling-bugs-in-kubernetes-integration.yml b/changelogs/unreleased/fix-error-handling-bugs-in-kubernetes-integration.yml
new file mode 100644
index 00000000000..f2a117fe63f
--- /dev/null
+++ b/changelogs/unreleased/fix-error-handling-bugs-in-kubernetes-integration.yml
@@ -0,0 +1,5 @@
+---
+title: Fix error handling bugs in kubernetes integration
+merge_request: 22922
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-stuck-import-jobs-query-performance-issue.yml b/changelogs/unreleased/fix-stuck-import-jobs-query-performance-issue.yml
new file mode 100644
index 00000000000..d8455a8509f
--- /dev/null
+++ b/changelogs/unreleased/fix-stuck-import-jobs-query-performance-issue.yml
@@ -0,0 +1,5 @@
+---
+title: Improves performance of stuck import jobs detection
+merge_request: 22879
+author:
+type: performance
diff --git a/changelogs/unreleased/fix-tags-for-envs.yml b/changelogs/unreleased/fix-tags-for-envs.yml
new file mode 100644
index 00000000000..633788ff6d8
--- /dev/null
+++ b/changelogs/unreleased/fix-tags-for-envs.yml
@@ -0,0 +1,5 @@
+---
+title: Do not reload self on hooks when creating deployment
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-41213-api-update-submodule-commit.yml b/changelogs/unreleased/fj-41213-api-update-submodule-commit.yml
new file mode 100644
index 00000000000..c06b02b05e8
--- /dev/null
+++ b/changelogs/unreleased/fj-41213-api-update-submodule-commit.yml
@@ -0,0 +1,5 @@
+---
+title: Add endpoint to update a git submodule reference
+merge_request: 20949
+author:
+type: added
diff --git a/changelogs/unreleased/fj-50890-fix-commit-message-wiki-new-page.yml b/changelogs/unreleased/fj-50890-fix-commit-message-wiki-new-page.yml
new file mode 100644
index 00000000000..5add6d727ac
--- /dev/null
+++ b/changelogs/unreleased/fj-50890-fix-commit-message-wiki-new-page.yml
@@ -0,0 +1,5 @@
+---
+title: Fix bug with wiki page create message
+merge_request: 22849
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-bump-gitaly-0-129-0.yml b/changelogs/unreleased/fj-bump-gitaly-0-129-0.yml
new file mode 100644
index 00000000000..9d44e46c0ed
--- /dev/null
+++ b/changelogs/unreleased/fj-bump-gitaly-0-129-0.yml
@@ -0,0 +1,5 @@
+---
+title: Bump Gitaly to 0.129.0
+merge_request: 22868
+author:
+type: added
diff --git a/changelogs/unreleased/fl-missing-i18n.yml b/changelogs/unreleased/fl-missing-i18n.yml
new file mode 100644
index 00000000000..d41a691e636
--- /dev/null
+++ b/changelogs/unreleased/fl-missing-i18n.yml
@@ -0,0 +1,5 @@
+---
+title: Adds missing i18n to pipelines table
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/frozen-string-enable-lib-gitlab-ci-remain.yml b/changelogs/unreleased/frozen-string-enable-lib-gitlab-ci-remain.yml
new file mode 100644
index 00000000000..ecbfc323080
--- /dev/null
+++ b/changelogs/unreleased/frozen-string-enable-lib-gitlab-ci-remain.yml
@@ -0,0 +1,5 @@
+---
+title: Enable frozen string for remaining lib/gitlab/ci/**/*.rb
+merge_request:
+author: gfyoung
+type: performance
diff --git a/changelogs/unreleased/frozen-string-enable-lib-gitlab-ci.yml b/changelogs/unreleased/frozen-string-enable-lib-gitlab-ci.yml
new file mode 100644
index 00000000000..a881c304f34
--- /dev/null
+++ b/changelogs/unreleased/frozen-string-enable-lib-gitlab-ci.yml
@@ -0,0 +1,5 @@
+---
+title: Enable frozen string for lib/gitlab/ci
+merge_request:
+author: gfyoung
+type: performance
diff --git a/changelogs/unreleased/frozen-string-lib-gitlab-more.yml b/changelogs/unreleased/frozen-string-lib-gitlab-more.yml
new file mode 100644
index 00000000000..cfbc4ced635
--- /dev/null
+++ b/changelogs/unreleased/frozen-string-lib-gitlab-more.yml
@@ -0,0 +1,5 @@
+---
+title: Enable even more frozen string in lib/gitlab/**/*.rb
+merge_request:
+author: gfyoung
+type: performance
diff --git a/changelogs/unreleased/gl-ui-loading-icon.yml b/changelogs/unreleased/gl-ui-loading-icon.yml
new file mode 100644
index 00000000000..5540fc7d7ea
--- /dev/null
+++ b/changelogs/unreleased/gl-ui-loading-icon.yml
@@ -0,0 +1,5 @@
+---
+title: Remove gitlab-ui's loading icon from global
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/gl-ui-modal.yml b/changelogs/unreleased/gl-ui-modal.yml
new file mode 100644
index 00000000000..fbdb8260d24
--- /dev/null
+++ b/changelogs/unreleased/gl-ui-modal.yml
@@ -0,0 +1,5 @@
+---
+title: Remove gitlab-ui's modal from global
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/gl-ui-pagination.yml b/changelogs/unreleased/gl-ui-pagination.yml
new file mode 100644
index 00000000000..cf73d6a1f8f
--- /dev/null
+++ b/changelogs/unreleased/gl-ui-pagination.yml
@@ -0,0 +1,5 @@
+---
+title: Remove gitlab-ui's pagination from global
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/gl-ui-progress-bar.yml b/changelogs/unreleased/gl-ui-progress-bar.yml
new file mode 100644
index 00000000000..1e584dacd6f
--- /dev/null
+++ b/changelogs/unreleased/gl-ui-progress-bar.yml
@@ -0,0 +1,5 @@
+---
+title: Remove gitlab-ui's progress bar from global
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/gl-ui-tooltip.yml b/changelogs/unreleased/gl-ui-tooltip.yml
new file mode 100644
index 00000000000..99ded9f812e
--- /dev/null
+++ b/changelogs/unreleased/gl-ui-tooltip.yml
@@ -0,0 +1,5 @@
+---
+title: Remove gitlab-ui's tooltip from global
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/gt-align-sign-in-button.yml b/changelogs/unreleased/gt-align-sign-in-button.yml
new file mode 100644
index 00000000000..a51fa319231
--- /dev/null
+++ b/changelogs/unreleased/gt-align-sign-in-button.yml
@@ -0,0 +1,5 @@
+---
+title: Align sign in button
+merge_request: 22888
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/gt-fix-ide-typos-in-props.yml b/changelogs/unreleased/gt-fix-ide-typos-in-props.yml
new file mode 100644
index 00000000000..a81b227c82f
--- /dev/null
+++ b/changelogs/unreleased/gt-fix-ide-typos-in-props.yml
@@ -0,0 +1,5 @@
+---
+title: Fix IDE typos in props
+merge_request: 22685
+author: George Tsiolis
+type: other
diff --git a/changelogs/unreleased/gt-fix-quick-links-button-styles.yml b/changelogs/unreleased/gt-fix-quick-links-button-styles.yml
new file mode 100644
index 00000000000..4c1150631f8
--- /dev/null
+++ b/changelogs/unreleased/gt-fix-quick-links-button-styles.yml
@@ -0,0 +1,5 @@
+---
+title: Fix quick links button styles
+merge_request: 22657
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/gt-update-project-and-group-labels-empty-state.yml b/changelogs/unreleased/gt-update-project-and-group-labels-empty-state.yml
new file mode 100644
index 00000000000..d644ca86b79
--- /dev/null
+++ b/changelogs/unreleased/gt-update-project-and-group-labels-empty-state.yml
@@ -0,0 +1,5 @@
+---
+title: Update project and group labels empty state
+merge_request: 22745
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/gt-use-merge-request-prefix-in-event-feed-title.yml b/changelogs/unreleased/gt-use-merge-request-prefix-in-event-feed-title.yml
new file mode 100644
index 00000000000..51af2807a03
--- /dev/null
+++ b/changelogs/unreleased/gt-use-merge-request-prefix-in-event-feed-title.yml
@@ -0,0 +1,5 @@
+---
+title: Use merge request prefix symbol in event feed title
+merge_request: 22449
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/ignore-environment-validation-failure.yml b/changelogs/unreleased/ignore-environment-validation-failure.yml
new file mode 100644
index 00000000000..1b61cf86dc4
--- /dev/null
+++ b/changelogs/unreleased/ignore-environment-validation-failure.yml
@@ -0,0 +1,5 @@
+---
+title: Ignore environment validation failure
+merge_request: 23100
+author:
+type: fixed
diff --git a/changelogs/unreleased/introduce-knative-support.yml b/changelogs/unreleased/introduce-knative-support.yml
new file mode 100644
index 00000000000..53290d71977
--- /dev/null
+++ b/changelogs/unreleased/introduce-knative-support.yml
@@ -0,0 +1,5 @@
+---
+title: Introduce Knative support
+author: Chris Baumbauer
+merge_request: 43959
+type: added
diff --git a/changelogs/unreleased/issue_51323.yml b/changelogs/unreleased/issue_51323.yml
new file mode 100644
index 00000000000..b0e83e303d1
--- /dev/null
+++ b/changelogs/unreleased/issue_51323.yml
@@ -0,0 +1,5 @@
+---
+title: Add 'only history' option to notes filter
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/kinolaev-master-patch-91872.yml b/changelogs/unreleased/kinolaev-master-patch-91872.yml
new file mode 100644
index 00000000000..053e9101e39
--- /dev/null
+++ b/changelogs/unreleased/kinolaev-master-patch-91872.yml
@@ -0,0 +1,5 @@
+---
+title: Change HELM_HOST in Auto-DevOps template to work behind proxy
+merge_request: 22596
+author: Sergej Nikolaev <kinolaev@gmail.com>
+type: fixed
diff --git a/changelogs/unreleased/kubernetes-http-response-code.yml b/changelogs/unreleased/kubernetes-http-response-code.yml
new file mode 100644
index 00000000000..551fe2edc3c
--- /dev/null
+++ b/changelogs/unreleased/kubernetes-http-response-code.yml
@@ -0,0 +1,5 @@
+---
+title: Show HTTP response code for Kubernetes errors
+merge_request: 22964
+author:
+type: other
diff --git a/changelogs/unreleased/max_retries_when.yml b/changelogs/unreleased/max_retries_when.yml
new file mode 100644
index 00000000000..dad3cd8a123
--- /dev/null
+++ b/changelogs/unreleased/max_retries_when.yml
@@ -0,0 +1,5 @@
+---
+title: Allow to configure when to retry failed CI jobs
+merge_request: 21758
+author: Markus Doits
+type: added
diff --git a/changelogs/unreleased/mr-file-tree-inline-fluid-width-fix.yml b/changelogs/unreleased/mr-file-tree-inline-fluid-width-fix.yml
deleted file mode 100644
index b61f47724fc..00000000000
--- a/changelogs/unreleased/mr-file-tree-inline-fluid-width-fix.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed merge request fill tree toggling not respecting fluid width preference
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/mr-image-commenting.yml b/changelogs/unreleased/mr-image-commenting.yml
new file mode 100644
index 00000000000..3cc3becc795
--- /dev/null
+++ b/changelogs/unreleased/mr-image-commenting.yml
@@ -0,0 +1,5 @@
+---
+title: Reimplemented image commenting in merge request diffs
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/mr-tree-filter-path-name.yml b/changelogs/unreleased/mr-tree-filter-path-name.yml
new file mode 100644
index 00000000000..152f8a67337
--- /dev/null
+++ b/changelogs/unreleased/mr-tree-filter-path-name.yml
@@ -0,0 +1,5 @@
+---
+title: Changed merge request filtering to be by path instead of name
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/osw-comment-on-any-line-on-diffs.yml b/changelogs/unreleased/osw-comment-on-any-line-on-diffs.yml
new file mode 100644
index 00000000000..e25d64a89d7
--- /dev/null
+++ b/changelogs/unreleased/osw-comment-on-any-line-on-diffs.yml
@@ -0,0 +1,5 @@
+---
+title: Allow commenting on any diff line in Merge Requests
+merge_request: 22914
+author:
+type: added
diff --git a/changelogs/unreleased/osw-fallback-on-blank-refs.yml b/changelogs/unreleased/osw-fallback-on-blank-refs.yml
new file mode 100644
index 00000000000..039179f5829
--- /dev/null
+++ b/changelogs/unreleased/osw-fallback-on-blank-refs.yml
@@ -0,0 +1,5 @@
+---
+title: Avoid Gitaly RPC errors when fetching diff stats
+merge_request: 22995
+author:
+type: fixed
diff --git a/changelogs/unreleased/rails5-mysql-milliseconds-deployment-spec.yml b/changelogs/unreleased/rails5-mysql-milliseconds-deployment-spec.yml
new file mode 100644
index 00000000000..8c71ecebfdb
--- /dev/null
+++ b/changelogs/unreleased/rails5-mysql-milliseconds-deployment-spec.yml
@@ -0,0 +1,5 @@
+---
+title: 'Rails5: fix mysql milliseconds issue in deployment model specs'
+merge_request: 22850
+author: Jasper Maes
+type: other
diff --git a/changelogs/unreleased/rake-gitaly-check.yml b/changelogs/unreleased/rake-gitaly-check.yml
new file mode 100644
index 00000000000..90fbd62d203
--- /dev/null
+++ b/changelogs/unreleased/rake-gitaly-check.yml
@@ -0,0 +1,5 @@
+---
+title: Add gitlab:gitaly:check task for Gitaly health check
+merge_request: 22063
+author:
+type: other
diff --git a/changelogs/unreleased/refactor-snippets-finder.yml b/changelogs/unreleased/refactor-snippets-finder.yml
new file mode 100644
index 00000000000..37cacf71c14
--- /dev/null
+++ b/changelogs/unreleased/refactor-snippets-finder.yml
@@ -0,0 +1,5 @@
+---
+title: Rewrite SnippetsFinder to improve performance by a factor of 1500
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/related_mrs.yml b/changelogs/unreleased/related_mrs.yml
new file mode 100644
index 00000000000..cc89e9d0cdb
--- /dev/null
+++ b/changelogs/unreleased/related_mrs.yml
@@ -0,0 +1,5 @@
+---
+title: Add API endpoint to list issue related merge requests
+merge_request: 21806
+author: Helmut Januschka
+type: added
diff --git a/changelogs/unreleased/remove-asset-sync.yml b/changelogs/unreleased/remove-asset-sync.yml
new file mode 100644
index 00000000000..ddb82212975
--- /dev/null
+++ b/changelogs/unreleased/remove-asset-sync.yml
@@ -0,0 +1,5 @@
+---
+title: Remove asset_sync gem from Gemfile and related code from codebase
+merge_request: 22610
+author:
+type: other
diff --git a/changelogs/unreleased/remove-ci_enable_scheduled_build-feature-flag.yml b/changelogs/unreleased/remove-ci_enable_scheduled_build-feature-flag.yml
new file mode 100644
index 00000000000..ce52a487551
--- /dev/null
+++ b/changelogs/unreleased/remove-ci_enable_scheduled_build-feature-flag.yml
@@ -0,0 +1,5 @@
+---
+title: Remove `ci_enable_scheduled_build` feature flag
+merge_request: 22742
+author:
+type: other
diff --git a/changelogs/unreleased/remove-duplicate-primary-button-in-dashboard-snippets.yml b/changelogs/unreleased/remove-duplicate-primary-button-in-dashboard-snippets.yml
new file mode 100644
index 00000000000..3a8b3a0df5d
--- /dev/null
+++ b/changelogs/unreleased/remove-duplicate-primary-button-in-dashboard-snippets.yml
@@ -0,0 +1,5 @@
+---
+title: Remove duplicate primary button in dashboard snippets on small viewports
+merge_request: 22902
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/remove-experimental-label-from-cluster-views.yml b/changelogs/unreleased/remove-experimental-label-from-cluster-views.yml
new file mode 100644
index 00000000000..af9512b27e9
--- /dev/null
+++ b/changelogs/unreleased/remove-experimental-label-from-cluster-views.yml
@@ -0,0 +1,5 @@
+---
+title: Removes experimental labels from cluster views
+merge_request: 22550
+author:
+type: other
diff --git a/changelogs/unreleased/replace-tooltip-in-markdown-component.yml b/changelogs/unreleased/replace-tooltip-in-markdown-component.yml
new file mode 100644
index 00000000000..5047e75c06a
--- /dev/null
+++ b/changelogs/unreleased/replace-tooltip-in-markdown-component.yml
@@ -0,0 +1,5 @@
+---
+title: Replace tooltip in markdown component with gl-tooltip
+merge_request: 21989
+author: George Tsiolis
+type: other
diff --git a/changelogs/unreleased/rs-cherry-pick-api.yml b/changelogs/unreleased/rs-cherry-pick-api.yml
new file mode 100644
index 00000000000..ce844dfc939
--- /dev/null
+++ b/changelogs/unreleased/rs-cherry-pick-api.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve possible cherry pick API race condition
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/rs-revert-api.yml b/changelogs/unreleased/rs-revert-api.yml
new file mode 100644
index 00000000000..c07b2fe624c
--- /dev/null
+++ b/changelogs/unreleased/rs-revert-api.yml
@@ -0,0 +1,5 @@
+---
+title: Add revert to commits API
+merge_request: 22919
+author:
+type: added
diff --git a/changelogs/unreleased/scheduled-manual-jobs-environment-play-buttons.yml b/changelogs/unreleased/scheduled-manual-jobs-environment-play-buttons.yml
new file mode 100644
index 00000000000..c89af78d989
--- /dev/null
+++ b/changelogs/unreleased/scheduled-manual-jobs-environment-play-buttons.yml
@@ -0,0 +1,5 @@
+---
+title: Add the Play button for delayed jobs in environment page
+merge_request: 22106
+author:
+type: added
diff --git a/changelogs/unreleased/security-kubeclient-ssrf.yml b/changelogs/unreleased/security-kubeclient-ssrf.yml
new file mode 100644
index 00000000000..45fc41029fc
--- /dev/null
+++ b/changelogs/unreleased/security-kubeclient-ssrf.yml
@@ -0,0 +1,5 @@
+---
+title: Monkey kubeclient to not follow any redirects.
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/sh-53180-append-path.yml b/changelogs/unreleased/sh-53180-append-path.yml
new file mode 100644
index 00000000000..64fae5522d8
--- /dev/null
+++ b/changelogs/unreleased/sh-53180-append-path.yml
@@ -0,0 +1,5 @@
+---
+title: Make sure there's only one slash as path separator
+merge_request: 22954
+author:
+type: other
diff --git a/changelogs/unreleased/sh-bump-ruby-2-5-3.yml b/changelogs/unreleased/sh-bump-ruby-2-5-3.yml
new file mode 100644
index 00000000000..13cadc73e9c
--- /dev/null
+++ b/changelogs/unreleased/sh-bump-ruby-2-5-3.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade to Ruby 2.5.3
+merge_request: 2806
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-fix-issue-52176.yml b/changelogs/unreleased/sh-fix-issue-52176.yml
new file mode 100644
index 00000000000..7269e14d910
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-issue-52176.yml
@@ -0,0 +1,5 @@
+---
+title: Disable replication lag check for Aurora PostgreSQL databases
+merge_request: 22786
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-issue-52649.yml b/changelogs/unreleased/sh-fix-issue-52649.yml
new file mode 100644
index 00000000000..34b7f74a345
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-issue-52649.yml
@@ -0,0 +1,5 @@
+---
+title: Fix statement timeouts in RemoveRestrictedTodos migration
+merge_request: 22795
+author:
+type: other
diff --git a/changelogs/unreleased/sh-fix-search-relative-urls.yml b/changelogs/unreleased/sh-fix-search-relative-urls.yml
new file mode 100644
index 00000000000..2545e9ca553
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-search-relative-urls.yml
@@ -0,0 +1,5 @@
+---
+title: Fix search "all in GitLab" not working with relative URLs
+merge_request: 22644
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-optimize-merge-request-project-lookup.yml b/changelogs/unreleased/sh-optimize-merge-request-project-lookup.yml
new file mode 100644
index 00000000000..241b89c4633
--- /dev/null
+++ b/changelogs/unreleased/sh-optimize-merge-request-project-lookup.yml
@@ -0,0 +1,5 @@
+---
+title: Reduce SQL queries needed to load open merge requests
+merge_request: 22709
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-optimize-mr-commit-sha-lookup.yml b/changelogs/unreleased/sh-optimize-mr-commit-sha-lookup.yml
new file mode 100644
index 00000000000..bea73f8d329
--- /dev/null
+++ b/changelogs/unreleased/sh-optimize-mr-commit-sha-lookup.yml
@@ -0,0 +1,5 @@
+---
+title: Optimize merge request refresh by using the database to check commit SHAs
+merge_request: 22731
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-optimize-reload-diffs-service.yml b/changelogs/unreleased/sh-optimize-reload-diffs-service.yml
new file mode 100644
index 00000000000..422102560ed
--- /dev/null
+++ b/changelogs/unreleased/sh-optimize-reload-diffs-service.yml
@@ -0,0 +1,5 @@
+---
+title: Significantly cut memory usage and SQL queries when reloading diffs
+merge_request: 22725
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-paginate-bitbucket-server-imports.yml b/changelogs/unreleased/sh-paginate-bitbucket-server-imports.yml
new file mode 100644
index 00000000000..b5743e71cf9
--- /dev/null
+++ b/changelogs/unreleased/sh-paginate-bitbucket-server-imports.yml
@@ -0,0 +1,5 @@
+---
+title: Paginate Bitbucket Server importer projects
+merge_request: 22825
+author:
+type: changed
diff --git a/changelogs/unreleased/stateful_deployments.yml b/changelogs/unreleased/stateful_deployments.yml
new file mode 100644
index 00000000000..4caa5ad77b8
--- /dev/null
+++ b/changelogs/unreleased/stateful_deployments.yml
@@ -0,0 +1,5 @@
+---
+title: Add status to Deployment
+merge_request: 22380
+author:
+type: changed
diff --git a/changelogs/unreleased/switch-rails.yml b/changelogs/unreleased/switch-rails.yml
new file mode 100644
index 00000000000..4edf709dbd4
--- /dev/null
+++ b/changelogs/unreleased/switch-rails.yml
@@ -0,0 +1,5 @@
+---
+title: Switch to Rails 5
+merge_request: 21492
+author:
+type: other
diff --git a/changelogs/unreleased/tc-index-uploads-file-store.yml b/changelogs/unreleased/tc-index-uploads-file-store.yml
new file mode 100644
index 00000000000..fa3b3164e38
--- /dev/null
+++ b/changelogs/unreleased/tc-index-uploads-file-store.yml
@@ -0,0 +1,5 @@
+---
+title: Enhance performance of counting local Uploads
+merge_request: 22522
+author:
+type: performance
diff --git a/changelogs/unreleased/toggle-sidebar-alignment.yml b/changelogs/unreleased/toggle-sidebar-alignment.yml
new file mode 100644
index 00000000000..428fe61da9b
--- /dev/null
+++ b/changelogs/unreleased/toggle-sidebar-alignment.yml
@@ -0,0 +1,5 @@
+---
+title: Align toggle sidebar button across all browsers and OSs
+merge_request: 22771
+author:
+type: fixed
diff --git a/changelogs/unreleased/top_level_clusters_controller.yml b/changelogs/unreleased/top_level_clusters_controller.yml
new file mode 100644
index 00000000000..1fe1d048de4
--- /dev/null
+++ b/changelogs/unreleased/top_level_clusters_controller.yml
@@ -0,0 +1,6 @@
+---
+title: Change to top level controller for clusters so that we can use it for project
+ clusters (now) and group clusters (later)
+merge_request: 22438
+author:
+type: other
diff --git a/changelogs/unreleased/triggermesh-phase2-serverless.yml b/changelogs/unreleased/triggermesh-phase2-serverless.yml
new file mode 100644
index 00000000000..bee2b5e1e2c
--- /dev/null
+++ b/changelogs/unreleased/triggermesh-phase2-serverless.yml
@@ -0,0 +1,5 @@
+---
+title: Add knative client to kubeclient library
+merge_request: 22968
+author: cab105
+type: added
diff --git a/changelogs/unreleased/update_license_management_job.yml b/changelogs/unreleased/update_license_management_job.yml
new file mode 100644
index 00000000000..d6e56080e77
--- /dev/null
+++ b/changelogs/unreleased/update_license_management_job.yml
@@ -0,0 +1,5 @@
+---
+title: "Remove dind from license_management auto-devops job definition"
+merge_request: 22732
+author:
+type: performance
diff --git a/changelogs/unreleased/upgrade-workhorse-7-1-0.yml b/changelogs/unreleased/upgrade-workhorse-7-1-0.yml
new file mode 100644
index 00000000000..b6df35e6d10
--- /dev/null
+++ b/changelogs/unreleased/upgrade-workhorse-7-1-0.yml
@@ -0,0 +1,5 @@
+---
+title: Update GitLab-Workhorse to v7.1.0
+merge_request: 22883
+author:
+type: other
diff --git a/changelogs/unreleased/winh-delayed-jobs-dynamic-timer.yml b/changelogs/unreleased/winh-delayed-jobs-dynamic-timer.yml
new file mode 100644
index 00000000000..fbedd2796b2
--- /dev/null
+++ b/changelogs/unreleased/winh-delayed-jobs-dynamic-timer.yml
@@ -0,0 +1,5 @@
+---
+title: Add dynamic timer to delayed jobs
+merge_request: 22382
+author:
+type: changed
diff --git a/changelogs/unreleased/winh-job-list-dynamic-timer.yml b/changelogs/unreleased/winh-job-list-dynamic-timer.yml
new file mode 100644
index 00000000000..333a974d6aa
--- /dev/null
+++ b/changelogs/unreleased/winh-job-list-dynamic-timer.yml
@@ -0,0 +1,5 @@
+---
+title: Add dynamic timer for delayed jobs in job list
+merge_request: 22656
+author:
+type: changed
diff --git a/changelogs/unreleased/zj-bump-gitaly-0-128.yml b/changelogs/unreleased/zj-bump-gitaly-0-128.yml
new file mode 100644
index 00000000000..451df4b800e
--- /dev/null
+++ b/changelogs/unreleased/zj-bump-gitaly-0-128.yml
@@ -0,0 +1,5 @@
+---
+title: Bump Gitaly to 0.128.0
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/zj-remove-broken-storage.yml b/changelogs/unreleased/zj-remove-broken-storage.yml
new file mode 100644
index 00000000000..9df87b40e09
--- /dev/null
+++ b/changelogs/unreleased/zj-remove-broken-storage.yml
@@ -0,0 +1,5 @@
+---
+title: Remove obsolete gitlab_shell rake tasks
+merge_request: 22417
+author:
+type: removed
diff --git a/config/application.rb b/config/application.rb
index 9074cf02c46..1b084e91cfb 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -8,7 +8,7 @@ module Gitlab
# This method is used for smooth upgrading from the current Rails 4.x to Rails 5.0.
# https://gitlab.com/gitlab-org/gitlab-ce/issues/14286
def self.rails5?
- ENV["RAILS5"].in?(%w[1 true])
+ !%w[0 false].include?(ENV["RAILS5"])
end
class Application < Rails::Application
@@ -144,7 +144,7 @@ module Gitlab
config.assets.precompile << "errors.css"
# Import gitlab-svgs directly from vendored directory
- config.assets.paths << "#{config.root}/node_modules/@gitlab-org/gitlab-svgs/dist"
+ config.assets.paths << "#{config.root}/node_modules/@gitlab/svgs/dist"
config.assets.precompile << "icons.svg"
config.assets.precompile << "icons.json"
config.assets.precompile << "illustrations/*.svg"
diff --git a/config/boot.rb b/config/boot.rb
index 655c54ddb84..1aeacdabbad 100644
--- a/config/boot.rb
+++ b/config/boot.rb
@@ -1,10 +1,10 @@
def rails5?
- %w[1 true].include?(ENV["RAILS5"])
+ !%w[0 false].include?(ENV["RAILS5"])
end
require 'rubygems' unless rails5?
-gemfile = rails5? ? "Gemfile.rails5" : "Gemfile"
+gemfile = rails5? ? "Gemfile" : "Gemfile.rails4"
ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../#{gemfile}", __dir__)
# Set up gems listed in the Gemfile.
diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml
index 62760ffee3a..488728e26ab 100644
--- a/config/dependency_decisions.yml
+++ b/config/dependency_decisions.yml
@@ -461,7 +461,7 @@
:versions: []
:when: 2017-09-13 17:31:16.425819400 Z
- - :license
- - "@gitlab-org/gitlab-svgs"
+ - "@gitlab/svgs"
- MIT
- :who: Tim Zallmann
:why: Our own library - GitLab License https://gitlab.com/gitlab-org/gitlab-svgs
diff --git a/config/environment.rb b/config/environment.rb
index 5d35937f7c6..3a52656a2c1 100644
--- a/config/environment.rb
+++ b/config/environment.rb
@@ -1,10 +1,10 @@
# Load the rails application
# Remove this condition when upgraded to rails 5.0.
-if %w[1 true].include?(ENV["RAILS5"])
- require_relative 'application'
-else
+if %w[0 false].include?(ENV["RAILS5"])
require File.expand_path('application', __dir__)
+else
+ require_relative 'application'
end
# Initialize the rails application
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 23790b84e3c..494ddd72556 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -45,4 +45,6 @@ Rails.application.configure do
# Do not log asset requests
config.assets.quiet = true
+
+ config.allow_concurrency = defined?(::Puma)
end
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 9941987929c..71195164e7a 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -83,5 +83,5 @@ Rails.application.configure do
config.eager_load = true
- config.allow_concurrency = false
+ config.allow_concurrency = defined?(::Puma)
end
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 5390ea78e62..a59600763fb 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -207,6 +207,10 @@ production: &base
# endpoint: 'http://127.0.0.1:9000' # default: nil
# path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object'
+ ## Packages (maven repository so far)
+ packages:
+ enabled: false
+
## GitLab Pages
pages:
enabled: false
@@ -587,7 +591,7 @@ production: &base
gitaly:
# Path to the directory containing Gitaly client executables.
client_path: /home/git/gitaly/bin
- # Default Gitaly authentication token. Can be overriden per storage. Can
+ # Default Gitaly authentication token. Can be overridden per storage. Can
# be left blank when Gitaly is running locally on a Unix socket, which
# is the normal way to deploy Gitaly.
token:
diff --git a/config/initializers/8_metrics.rb b/config/initializers/8_metrics.rb
index c8d261d415e..468f80939d7 100644
--- a/config/initializers/8_metrics.rb
+++ b/config/initializers/8_metrics.rb
@@ -98,7 +98,11 @@ end
# check: https://github.com/rspec/rspec-mocks#settings-mocks-or-stubs-on-any-instance-of-a-class
#
# Related issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/33587
-if Gitlab::Metrics.enabled? && !Rails.env.test?
+#
+# In development mode, we turn off eager loading when we're running
+# `rails generate migration` because eager loading short-circuits the
+# loading of our custom migration templates.
+if Gitlab::Metrics.enabled? && !Rails.env.test? && !(Rails.env.development? && defined?(Rails::Generators))
require 'pathname'
require 'influxdb'
require 'connection_pool'
diff --git a/config/initializers/asset_sync.rb b/config/initializers/asset_sync.rb
deleted file mode 100644
index 7f3934853fa..00000000000
--- a/config/initializers/asset_sync.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-AssetSync.configure do |config|
- # Disable the asset_sync gem by default. If it is enabled, but not configured,
- # asset_sync will cause the build to fail.
- config.enabled = if ENV.has_key?('ASSET_SYNC_ENABLED')
- ENV['ASSET_SYNC_ENABLED'] == 'true'
- else
- false
- end
-
- # Pulled from https://github.com/AssetSync/asset_sync/blob/v2.2.0/lib/asset_sync/engine.rb#L15-L40
- # This allows us to disable asset_sync by default and configure through environment variables
- # Updates to asset_sync gem should be checked
- config.fog_provider = ENV['FOG_PROVIDER'] if ENV.has_key?('FOG_PROVIDER')
- config.fog_directory = ENV['FOG_DIRECTORY'] if ENV.has_key?('FOG_DIRECTORY')
- config.fog_region = ENV['FOG_REGION'] if ENV.has_key?('FOG_REGION')
-
- config.aws_access_key_id = ENV['ASSETS_AWS_ACCESS_KEY_ID'] if ENV.has_key?('ASSETS_AWS_ACCESS_KEY_ID')
- config.aws_secret_access_key = ENV['ASSETS_AWS_SECRET_ACCESS_KEY'] if ENV.has_key?('ASSETS_AWS_SECRET_ACCESS_KEY')
- config.aws_reduced_redundancy = ENV['AWS_REDUCED_REDUNDANCY'] == true if ENV.has_key?('AWS_REDUCED_REDUNDANCY')
-
- config.rackspace_username = ENV['RACKSPACE_USERNAME'] if ENV.has_key?('RACKSPACE_USERNAME')
- config.rackspace_api_key = ENV['RACKSPACE_API_KEY'] if ENV.has_key?('RACKSPACE_API_KEY')
-
- config.google_storage_access_key_id = ENV['GOOGLE_STORAGE_ACCESS_KEY_ID'] if ENV.has_key?('GOOGLE_STORAGE_ACCESS_KEY_ID')
- config.google_storage_secret_access_key = ENV['GOOGLE_STORAGE_SECRET_ACCESS_KEY'] if ENV.has_key?('GOOGLE_STORAGE_SECRET_ACCESS_KEY')
-
- config.existing_remote_files = ENV['ASSET_SYNC_EXISTING_REMOTE_FILES'] || "keep"
-
- config.gzip_compression = (ENV['ASSET_SYNC_GZIP_COMPRESSION'] == 'true') if ENV.has_key?('ASSET_SYNC_GZIP_COMPRESSION')
- config.manifest = (ENV['ASSET_SYNC_MANIFEST'] == 'true') if ENV.has_key?('ASSET_SYNC_MANIFEST')
-end
diff --git a/config/initializers/fill_shards.rb b/config/initializers/fill_shards.rb
new file mode 100644
index 00000000000..18e067c8854
--- /dev/null
+++ b/config/initializers/fill_shards.rb
@@ -0,0 +1,3 @@
+if Shard.connected? && !Gitlab::Database.read_only?
+ Shard.populate!
+end
diff --git a/config/initializers/kubeclient.rb b/config/initializers/kubeclient.rb
index 7f115268b37..2d9f439fdc0 100644
--- a/config/initializers/kubeclient.rb
+++ b/config/initializers/kubeclient.rb
@@ -13,4 +13,25 @@ class Kubeclient::Client
ns_prefix = build_namespace_prefix(namespace)
rest_client["#{ns_prefix}#{entity_name_plural}/#{name}:#{port}/proxy"].url
end
+
+ # Monkey patch to set `max_redirects: 0`, so that kubeclient
+ # does not follow redirects and expose internal services.
+ # See https://gitlab.com/gitlab-org/gitlab-ce/issues/53158
+ def create_rest_client(path = nil)
+ path ||= @api_endpoint.path
+ options = {
+ ssl_ca_file: @ssl_options[:ca_file],
+ ssl_cert_store: @ssl_options[:cert_store],
+ verify_ssl: @ssl_options[:verify_ssl],
+ ssl_client_cert: @ssl_options[:client_cert],
+ ssl_client_key: @ssl_options[:client_key],
+ proxy: @http_proxy_uri,
+ user: @auth_options[:username],
+ password: @auth_options[:password],
+ open_timeout: @timeouts[:open],
+ read_timeout: @timeouts[:read],
+ max_redirects: 0
+ }
+ RestClient::Resource.new(@api_endpoint.merge(path).to_s, options)
+ end
end
diff --git a/config/initializers/mysql_set_length_for_binary_indexes.rb b/config/initializers/mysql_set_length_for_binary_indexes.rb
index 81ed2fb83de..0445d8fcae2 100644
--- a/config/initializers/mysql_set_length_for_binary_indexes.rb
+++ b/config/initializers/mysql_set_length_for_binary_indexes.rb
@@ -24,28 +24,46 @@ if defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
ActiveRecord::ConnectionAdapters::Mysql2Adapter.send(:prepend, MysqlSetLengthForBinaryIndex)
end
-if Gitlab.rails5?
- module MysqlSetLengthForBinaryIndexAndIgnorePostgresOptionsForSchema
- # This method is used in Rails 5 schema loading as t.index
- def index(column_names, options = {})
- options[:length] ||= {}
- Array(column_names).each do |column_name|
- column = columns.find { |c| c.name == column_name }
-
- if column&.type == :binary
- options[:length][column_name] = 20
- end
- end
+module MysqlSetLengthForBinaryIndexAndIgnorePostgresOptionsForSchema
+ # This method is used in Rails 5 schema loading as t.index
+ def index(column_names, options = {})
+ # Ignore indexes that use opclasses,
+ # also see config/initializers/mysql_ignore_postgresql_options.rb
+ if options[:opclasses]
+ warn "WARNING: index on columns #{column_names} uses unsupported option, skipping."
+ return
+ end
+
+ # when running rails 4 with rails 5 schema, rails 4 doesn't support multiple
+ # indexes on the same set of columns. Mysql doesn't support partial indexes, so if
+ # an index already exists and we add another index, skip it if it's partial:
+ # see https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21492#note_102821326
+ if !Gitlab.rails5? && indexes[column_names] && options[:where]
+ warn "WARNING: index on columns #{column_names} already exists and partial index is not supported, skipping."
+ return
+ end
+
+ options[:length] ||= {}
+ Array(column_names).each do |column_name|
+ column = columns.find { |c| c.name == column_name }
- # Ignore indexes that use opclasses,
- # also see config/initializers/mysql_ignore_postgresql_options.rb
- unless options[:opclasses]
- super(column_names, options)
+ if column&.type == :binary
+ options[:length][column_name] = 20
end
end
+
+ super(column_names, options)
end
+end
+def mysql_adapter?
+ defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter) && ActiveRecord::Base.connection.is_a?(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
+end
+
+if Gitlab.rails5?
if defined?(ActiveRecord::ConnectionAdapters::MySQL::TableDefinition)
ActiveRecord::ConnectionAdapters::MySQL::TableDefinition.send(:prepend, MysqlSetLengthForBinaryIndexAndIgnorePostgresOptionsForSchema)
end
+elsif mysql_adapter? && defined?(ActiveRecord::ConnectionAdapters::TableDefinition)
+ ActiveRecord::ConnectionAdapters::TableDefinition.send(:prepend, MysqlSetLengthForBinaryIndexAndIgnorePostgresOptionsForSchema)
end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 795e5d4e6bc..0a43a1d9a6b 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -8,6 +8,8 @@ en:
issue_link:
source: Source issue
target: Target issue
+ group:
+ path: Group URL
errors:
messages:
label_already_exists_at_group_level: "already exists at group level for %{group}. Please choose another one."
diff --git a/config/routes.rb b/config/routes.rb
index 37c7f98ec98..484e05114be 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -82,6 +82,30 @@ Rails.application.routes.draw do
draw :operations
draw :instance_statistics
+
+ if ENV['GITLAB_ENABLE_CHAOS_ENDPOINTS']
+ get '/chaos/leakmem' => 'chaos#leakmem'
+ get '/chaos/cpuspin' => 'chaos#cpuspin'
+ get '/chaos/sleep' => 'chaos#sleep'
+ get '/chaos/kill' => 'chaos#kill'
+ end
+ end
+
+ concern :clusterable do
+ resources :clusters, only: [:index, :new, :show, :update, :destroy] do
+ collection do
+ post :create_user
+ post :create_gcp
+ end
+
+ member do
+ scope :applications do
+ post '/:application', to: 'clusters/applications#create', as: :install_applications
+ end
+
+ get :cluster_status, format: :json
+ end
+ end
end
draw :api
diff --git a/config/routes/group.rb b/config/routes/group.rb
index 2328b50b760..a0aeebe4b91 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -53,6 +53,8 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
resource :avatar, only: [:destroy]
+ concerns :clusterable
+
resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
post :resend_invite, on: :member
delete :leave, on: :collection
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 73c46f72168..3f1ad90dfca 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -149,7 +149,9 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
scope path: 'merge_requests', controller: 'merge_requests/creations' do
post '', action: :create, as: nil
- scope path: 'new/(:merge_request_source_branch)', as: :new_merge_request do
+ scope path: 'new', as: :new_merge_request do
+ get '', action: :new
+
scope constraints: { format: nil }, action: :new do
get :diffs, defaults: { tab: 'diffs' }
get :pipelines, defaults: { tab: 'pipelines' }
@@ -163,7 +165,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get :diff_for_path
get :branch_from
get :branch_to
- get '', action: :new
end
end
@@ -206,20 +207,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
- resources :clusters, except: [:edit, :create] do
- collection do
- post :create_gcp
- post :create_user
- end
-
- member do
- get :status, format: :json
-
- scope :applications do
- post '/:application', to: 'clusters/applications#create', as: :install_applications
- end
- end
- end
+ concerns :clusterable
resources :environments, except: [:destroy] do
member do
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 0e723cdeb9c..53e1c8778b6 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -29,6 +29,7 @@
- [pipeline_creation, 4]
- [pipeline_default, 3]
- [pipeline_cache, 3]
+ - [deployment, 3]
- [pipeline_hooks, 2]
- [gitlab_shell, 2]
- [email_receiver, 2]
diff --git a/danger/documentation/Dangerfile b/danger/documentation/Dangerfile
index fe819ee250c..1ee025f0972 100644
--- a/danger/documentation/Dangerfile
+++ b/danger/documentation/Dangerfile
@@ -24,9 +24,32 @@ The following files require a review from the Documentation team:
* #{docs_paths_to_review.map { |path| "`#{path}`" }.join("\n* ")}
-To make sure these changes are reviewed, mention `@gl-docsteam` in a separate
-comment, and explain what needs to be reviewed by the team. Please don't mention
-the team until your changes are ready for review.
+When your content is ready for review, mention a technical writer in a separate
+comment and explain what needs to be reviewed.
+
+You are welcome to mention them sooner if you have questions about writing or updating
+the documentation. GitLabbers are also welcome to use the [#docs](https://gitlab.slack.com/archives/C16HYA2P5) channel on Slack.
+
+Who to ping [based on DevOps stages](https://about.gitlab.com/handbook/product/categories/#devops-stages):
+
+| Stage | Tech writer |
+| ------------- | ----------- |
+| ~Create | `@marcia` |
+| ~Configure | `@eread` |
+| ~Distribution | `@axil` |
+| ~Geo | `@eread` |
+| ~Gitaly | `@axil` |
+| ~Gitter | `@axil` |
+| ~Manage | `@eread` |
+| ~Monitoring | `@axil` |
+| ~Packaging | `@axil` |
+| ~Plan | `@mikelewis`|
+| ~Release | `marcia` |
+| ~Secure | `@axil` |
+| ~Verify | `@eread` |
+
+If you are not sure which category the change falls within, or the change is not
+part of one of these categories, you can mention the whole team with `@gl-docsteam`.
MARKDOWN
unless gitlab.mr_labels.include?('Documentation')
diff --git a/danger/metadata/Dangerfile b/danger/metadata/Dangerfile
index 51fc9e6bfca..1adca152736 100644
--- a/danger/metadata/Dangerfile
+++ b/danger/metadata/Dangerfile
@@ -23,3 +23,10 @@ has_pick_into_stable_label = gitlab.mr_labels.find { |label| label.start_with?('
if gitlab.branch_for_base != "master" && !has_pick_into_stable_label
warn "Most of the time, merge requests should target `master`. Otherwise, please set the relevant `Pick into X.Y` label."
end
+
+if gitlab.mr_json['title'].length > 72
+ warn 'The title of this merge request is longer than 72 characters and ' \
+ 'would violate our commit message rules when using the Squash on Merge ' \
+ 'feature. Please consider adjusting the title, or rebase the ' \
+ "commits manually and don't use Squash on Merge."
+end
diff --git a/db/fixtures/development/09_issues.rb b/db/fixtures/development/09_issues.rb
index 0b32a461d56..16243b72f9a 100644
--- a/db/fixtures/development/09_issues.rb
+++ b/db/fixtures/development/09_issues.rb
@@ -8,7 +8,8 @@ Gitlab::Seeder.quiet do
description: FFaker::Lorem.sentence,
state: ['opened', 'closed'].sample,
milestone: project.milestones.sample,
- assignees: [project.team.users.sample]
+ assignees: [project.team.users.sample],
+ created_at: rand(12).months.ago
}
Issues::CreateService.new(project, project.team.users.sample, issue_params).execute
diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb
index 285436f4324..7a86fe2eb7c 100644
--- a/db/fixtures/development/17_cycle_analytics.rb
+++ b/db/fixtures/development/17_cycle_analytics.rb
@@ -180,11 +180,8 @@ class Gitlab::Seeder::CycleAnalytics
ref: "refs/heads/#{merge_request.source_branch}")
pipeline = service.execute(:push, ignore_skip_ci: true, save_on_errors: false)
- pipeline.run!
- Timecop.travel rand(1..6).hours.from_now
- pipeline.succeed!
-
- PipelineMetricsWorker.new.perform(pipeline.id)
+ pipeline.builds.map(&:run!)
+ pipeline.update_status
end
end
@@ -204,7 +201,8 @@ class Gitlab::Seeder::CycleAnalytics
job = merge_request.head_pipeline.builds.where.not(environment: nil).last
- CreateDeploymentService.new(job).execute
+ job.success!
+ pipeline.update_status
end
end
end
diff --git a/db/migrate/20180413022611_create_missing_namespace_for_internal_users.rb b/db/migrate/20180413022611_create_missing_namespace_for_internal_users.rb
index 8fc558be733..b7b346cb10e 100644
--- a/db/migrate/20180413022611_create_missing_namespace_for_internal_users.rb
+++ b/db/migrate/20180413022611_create_missing_namespace_for_internal_users.rb
@@ -45,7 +45,7 @@ class CreateMissingNamespaceForInternalUsers < ActiveRecord::Migration
connection.exec_query(query).present?
end
- insert_query = "INSERT INTO namespaces(owner_id, path, name) VALUES(#{user_id}, '#{path}', '#{path}')"
+ insert_query = "INSERT INTO namespaces(owner_id, path, name, created_at, updated_at) VALUES(#{user_id}, '#{path}', '#{path}', NOW(), NOW())"
namespace_id = connection.insert_sql(insert_query)
create_route(namespace_id)
@@ -57,7 +57,7 @@ class CreateMissingNamespaceForInternalUsers < ActiveRecord::Migration
row = connection.exec_query("SELECT id, path FROM namespaces WHERE id=#{namespace_id}").first
id, path = row.values_at('id', 'path')
- execute("INSERT INTO routes(source_id, source_type, path, name) VALUES(#{id}, 'Namespace', '#{path}', '#{path}')")
+ execute("INSERT INTO routes(source_id, source_type, path, name, created_at, updated_at) VALUES(#{id}, 'Namespace', '#{path}', '#{path}', NOW(), NOW())")
end
def set_notification_email(user_id)
diff --git a/db/migrate/20180912111628_add_knative_application.rb b/db/migrate/20180912111628_add_knative_application.rb
new file mode 100644
index 00000000000..bfda6a945a7
--- /dev/null
+++ b/db/migrate/20180912111628_add_knative_application.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class AddKnativeApplication < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table "clusters_applications_knative" do |t|
+ t.references :cluster, null: false, unique: true, foreign_key: { on_delete: :cascade }
+
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.integer "status", null: false
+ t.string "version", null: false
+ t.string "hostname"
+ t.text "status_reason"
+ end
+ end
+end
diff --git a/db/migrate/20180927073410_add_index_to_project_deploy_tokens_deploy_token_id.rb b/db/migrate/20180927073410_add_index_to_project_deploy_tokens_deploy_token_id.rb
new file mode 100644
index 00000000000..61d32fe16eb
--- /dev/null
+++ b/db/migrate/20180927073410_add_index_to_project_deploy_tokens_deploy_token_id.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddIndexToProjectDeployTokensDeployTokenId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ # MySQL already has index inserted
+ add_concurrent_index :project_deploy_tokens, :deploy_token_id if Gitlab::Database.postgresql?
+ end
+
+ def down
+ remove_concurrent_index(:project_deploy_tokens, :deploy_token_id) if Gitlab::Database.postgresql?
+ end
+end
diff --git a/db/migrate/20181005125926_add_index_to_uploads_store.rb b/db/migrate/20181005125926_add_index_to_uploads_store.rb
new file mode 100644
index 00000000000..d32ca05e980
--- /dev/null
+++ b/db/migrate/20181005125926_add_index_to_uploads_store.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexToUploadsStore < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :uploads, :store
+ end
+
+ def down
+ remove_concurrent_index :uploads, :store
+ end
+end
diff --git a/db/migrate/20181015155839_add_finished_at_to_deployments.rb b/db/migrate/20181015155839_add_finished_at_to_deployments.rb
new file mode 100644
index 00000000000..1a061bb0f5f
--- /dev/null
+++ b/db/migrate/20181015155839_add_finished_at_to_deployments.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddFinishedAtToDeployments < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ add_column :deployments, :finished_at, :datetime_with_timezone
+ end
+
+ def down
+ remove_column :deployments, :finished_at, :datetime_with_timezone
+ end
+end
diff --git a/db/migrate/20181016141739_add_status_to_deployments.rb b/db/migrate/20181016141739_add_status_to_deployments.rb
new file mode 100644
index 00000000000..321172696b4
--- /dev/null
+++ b/db/migrate/20181016141739_add_status_to_deployments.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+class AddStatusToDeployments < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DEPLOYMENT_STATUS_SUCCESS = 2 # Equivalent to Deployment.state_machine.states['success'].value
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ ##
+ # NOTE:
+ # Ideally, `status` column should not have default value because it should be leveraged by state machine (i.e. application level).
+ # However, we have to use the default value for avoiding `NOT NULL` violation during the transition period.
+ # The default value should be removed in the future release.
+ def up
+ add_column_with_default(:deployments,
+ :status,
+ :integer,
+ limit: 2,
+ default: DEPLOYMENT_STATUS_SUCCESS,
+ allow_null: false)
+ end
+
+ def down
+ remove_column(:deployments, :status)
+ end
+end
diff --git a/db/migrate/20181019032400_add_shards_table.rb b/db/migrate/20181019032400_add_shards_table.rb
new file mode 100644
index 00000000000..5e0a6960548
--- /dev/null
+++ b/db/migrate/20181019032400_add_shards_table.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddShardsTable < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ create_table :shards do |t|
+ t.string :name, null: false, index: { unique: true }
+ end
+ end
+end
diff --git a/db/migrate/20181019032408_add_repositories_table.rb b/db/migrate/20181019032408_add_repositories_table.rb
new file mode 100644
index 00000000000..077f264d3ce
--- /dev/null
+++ b/db/migrate/20181019032408_add_repositories_table.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddRepositoriesTable < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ create_table :repositories, id: :bigserial do |t|
+ t.references :shard, null: false, index: true, foreign_key: { on_delete: :restrict }
+ t.string :disk_path, null: false, index: { unique: true }
+ end
+
+ add_column :projects, :pool_repository_id, :bigint
+ add_index :projects, :pool_repository_id, where: 'pool_repository_id IS NOT NULL'
+ end
+end
diff --git a/db/migrate/20181019105553_add_projects_pool_repository_id_foreign_key.rb b/db/migrate/20181019105553_add_projects_pool_repository_id_foreign_key.rb
new file mode 100644
index 00000000000..059988de38a
--- /dev/null
+++ b/db/migrate/20181019105553_add_projects_pool_repository_id_foreign_key.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class AddProjectsPoolRepositoryIdForeignKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key(
+ :projects,
+ :repositories,
+ column: :pool_repository_id,
+ on_delete: :nullify
+ )
+ end
+
+ def down
+ remove_foreign_key(:projects, column: :pool_repository_id)
+ end
+end
diff --git a/db/migrate/20181022135539_add_index_on_status_to_deployments.rb b/db/migrate/20181022135539_add_index_on_status_to_deployments.rb
new file mode 100644
index 00000000000..2eed20aa855
--- /dev/null
+++ b/db/migrate/20181022135539_add_index_on_status_to_deployments.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddIndexOnStatusToDeployments < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :deployments, [:project_id, :status]
+ add_concurrent_index :deployments, [:environment_id, :status]
+ end
+
+ def down
+ remove_concurrent_index :deployments, [:project_id, :status]
+ remove_concurrent_index :deployments, [:environment_id, :status]
+ end
+end
diff --git a/db/migrate/20181023104858_add_archive_builds_duration_to_application_settings.rb b/db/migrate/20181023104858_add_archive_builds_duration_to_application_settings.rb
new file mode 100644
index 00000000000..744748b3fad
--- /dev/null
+++ b/db/migrate/20181023104858_add_archive_builds_duration_to_application_settings.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddArchiveBuildsDurationToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column(:application_settings, :archive_builds_in_seconds, :integer, allow_null: true)
+ end
+end
diff --git a/db/migrate/20181023144439_add_partial_index_for_legacy_successful_deployments.rb b/db/migrate/20181023144439_add_partial_index_for_legacy_successful_deployments.rb
new file mode 100644
index 00000000000..5896102af1c
--- /dev/null
+++ b/db/migrate/20181023144439_add_partial_index_for_legacy_successful_deployments.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddPartialIndexForLegacySuccessfulDeployments < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ INDEX_NAME = 'partial_index_deployments_for_legacy_successful_deployments'.freeze
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index(:deployments, :id, where: "finished_at IS NULL AND status = 2", name: INDEX_NAME)
+ end
+
+ def down
+ remove_concurrent_index_by_name(:deployments, INDEX_NAME)
+ end
+end
diff --git a/db/migrate/20181025115728_add_private_commit_email_hostname_to_application_settings.rb b/db/migrate/20181025115728_add_private_commit_email_hostname_to_application_settings.rb
new file mode 100644
index 00000000000..89ddaf2ae2b
--- /dev/null
+++ b/db/migrate/20181025115728_add_private_commit_email_hostname_to_application_settings.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddPrivateCommitEmailHostnameToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column(:application_settings, :commit_email_hostname, :string, null: true)
+ end
+end
diff --git a/db/migrate/20181026143227_migrate_snippets_access_level_default_value.rb b/db/migrate/20181026143227_migrate_snippets_access_level_default_value.rb
new file mode 100644
index 00000000000..ede0ee27b8a
--- /dev/null
+++ b/db/migrate/20181026143227_migrate_snippets_access_level_default_value.rb
@@ -0,0 +1,42 @@
+# 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 MigrateSnippetsAccessLevelDefaultValue < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ ENABLED = 20
+
+ disable_ddl_transaction!
+
+ class ProjectFeature < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'project_features'
+ end
+
+ def up
+ change_column_default :project_features, :snippets_access_level, ENABLED
+
+ # On GitLab.com this will update about 28 000 rows. Since our updates are
+ # very small and this column is not indexed, these updates should be very
+ # lightweight.
+ ProjectFeature.where(snippets_access_level: nil).each_batch do |batch|
+ batch.update_all(snippets_access_level: ENABLED)
+ end
+
+ # We do not need to perform this in a post-deployment migration as the
+ # ProjectFeature model already enforces a default value for all new rows.
+ change_column_null :project_features, :snippets_access_level, false
+ end
+
+ def down
+ change_column_null :project_features, :snippets_access_level, true
+ change_column_default :project_features, :snippets_access_level, nil
+
+ # We can't migrate from 20 -> NULL, as some projects may have explicitly set
+ # the access level to 20.
+ end
+end
diff --git a/db/migrate/20181031190558_drop_fk_gcp_clusters_table.rb b/db/migrate/20181031190558_drop_fk_gcp_clusters_table.rb
new file mode 100644
index 00000000000..a7106111f46
--- /dev/null
+++ b/db/migrate/20181031190558_drop_fk_gcp_clusters_table.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+class DropFkGcpClustersTable < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ remove_foreign_key_if_exists :gcp_clusters, column: :project_id
+ remove_foreign_key_if_exists :gcp_clusters, column: :user_id
+ remove_foreign_key_if_exists :gcp_clusters, column: :service_id
+ end
+
+ def down
+ add_foreign_key_if_not_exists :gcp_clusters, :projects, column: :project_id, on_delete: :cascade
+ add_foreign_key_if_not_exists :gcp_clusters, :users, column: :user_id, on_delete: :nullify
+ add_foreign_key_if_not_exists :gcp_clusters, :services, column: :service_id, on_delete: :nullify
+ end
+
+ private
+
+ def add_foreign_key_if_not_exists(source, target, column:, on_delete:)
+ return unless table_exists?(source)
+ return if foreign_key_exists?(source, target, column: column)
+
+ add_concurrent_foreign_key(source, target, column: column, on_delete: on_delete)
+ end
+
+ def remove_foreign_key_if_exists(table, column:)
+ return unless table_exists?(table)
+ return unless foreign_key_exists?(table, column: column)
+
+ remove_foreign_key(table, column: column)
+ end
+end
diff --git a/db/migrate/20181031190559_drop_gcp_clusters_table.rb b/db/migrate/20181031190559_drop_gcp_clusters_table.rb
new file mode 100644
index 00000000000..808d474b4fc
--- /dev/null
+++ b/db/migrate/20181031190559_drop_gcp_clusters_table.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+class DropGcpClustersTable < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ drop_table :gcp_clusters
+ end
+
+ def down
+ create_table :gcp_clusters do |t|
+ # Order columns by best align scheme
+ t.references :project, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
+ t.references :user, foreign_key: { on_delete: :nullify }
+ t.references :service, foreign_key: { on_delete: :nullify }
+ t.integer :status
+ t.integer :gcp_cluster_size, null: false
+
+ # Timestamps
+ t.datetime_with_timezone :created_at, null: false
+ t.datetime_with_timezone :updated_at, null: false
+
+ # Enable/disable
+ t.boolean :enabled, default: true
+
+ # General
+ t.text :status_reason
+
+ # k8s integration specific
+ t.string :project_namespace
+
+ # Cluster details
+ t.string :endpoint
+ t.text :ca_cert
+ t.text :encrypted_kubernetes_token
+ t.string :encrypted_kubernetes_token_iv
+ t.string :username
+ t.text :encrypted_password
+ t.string :encrypted_password_iv
+
+ # GKE
+ t.string :gcp_project_id, null: false
+ t.string :gcp_cluster_zone, null: false
+ t.string :gcp_cluster_name, null: false
+ t.string :gcp_machine_type
+ t.string :gcp_operation_id
+ t.text :encrypted_gcp_token
+ t.string :encrypted_gcp_token_iv
+ end
+ end
+end
diff --git a/db/migrate/20181101144347_add_index_for_stuck_mr_query.rb b/db/migrate/20181101144347_add_index_for_stuck_mr_query.rb
new file mode 100644
index 00000000000..5d3ace54e5c
--- /dev/null
+++ b/db/migrate/20181101144347_add_index_for_stuck_mr_query.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+class AddIndexForStuckMrQuery < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :merge_requests, [:id, :merge_jid], where: "merge_jid IS NOT NULL and state = 'locked'"
+ end
+
+ def down
+ remove_concurrent_index :merge_requests, [:id, :merge_jid], where: "merge_jid IS NOT NULL and state = 'locked'"
+ end
+end
diff --git a/db/migrate/20181106135939_add_index_to_deployments.rb b/db/migrate/20181106135939_add_index_to_deployments.rb
new file mode 100644
index 00000000000..5f988a4723c
--- /dev/null
+++ b/db/migrate/20181106135939_add_index_to_deployments.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexToDeployments < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :deployments, [:project_id, :status, :created_at]
+ end
+
+ def down
+ remove_concurrent_index :deployments, [:project_id, :status, :created_at]
+ end
+end
diff --git a/db/post_migrate/20170717111152_cleanup_move_system_upload_folder_symlink.rb b/db/post_migrate/20170717111152_cleanup_move_system_upload_folder_symlink.rb
index c48f1c938d0..3ae4406ff96 100644
--- a/db/post_migrate/20170717111152_cleanup_move_system_upload_folder_symlink.rb
+++ b/db/post_migrate/20170717111152_cleanup_move_system_upload_folder_symlink.rb
@@ -13,7 +13,7 @@ class CleanupMoveSystemUploadFolderSymlink < ActiveRecord::Migration
say "Removing #{old_directory} -> #{new_directory} symlink"
FileUtils.rm(old_directory)
else
- say "Symlink #{old_directory} non existant, nothing to do."
+ say "Symlink #{old_directory} non existent, nothing to do."
end
end
diff --git a/db/post_migrate/20181022173835_enqueue_populate_cluster_kubernetes_namespace.rb b/db/post_migrate/20181022173835_enqueue_populate_cluster_kubernetes_namespace.rb
new file mode 100644
index 00000000000..f80a2aa6eac
--- /dev/null
+++ b/db/post_migrate/20181022173835_enqueue_populate_cluster_kubernetes_namespace.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class EnqueuePopulateClusterKubernetesNamespace < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ MIGRATION = 'PopulateClusterKubernetesNamespaceTable'.freeze
+
+ disable_ddl_transaction!
+
+ def up
+ BackgroundMigrationWorker.perform_async(MIGRATION)
+ end
+
+ def down
+ Clusters::KubernetesNamespace.delete_all
+ end
+end
diff --git a/db/post_migrate/20181030135124_fill_empty_finished_at_in_deployments.rb b/db/post_migrate/20181030135124_fill_empty_finished_at_in_deployments.rb
new file mode 100644
index 00000000000..32b271c472a
--- /dev/null
+++ b/db/post_migrate/20181030135124_fill_empty_finished_at_in_deployments.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class FillEmptyFinishedAtInDeployments < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ DEPLOYMENT_STATUS_SUCCESS = 2 # Equivalent to Deployment.statuses[:success]
+
+ class Deployments < ActiveRecord::Base
+ self.table_name = 'deployments'
+
+ include EachBatch
+ end
+
+ def up
+ FillEmptyFinishedAtInDeployments::Deployments
+ .where('finished_at IS NULL')
+ .where('status = ?', DEPLOYMENT_STATUS_SUCCESS)
+ .each_batch(of: 10_000) do |relation|
+ relation.update_all('finished_at=created_at')
+ end
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/post_migrate/20181105201455_steal_fill_store_upload.rb b/db/post_migrate/20181105201455_steal_fill_store_upload.rb
new file mode 100644
index 00000000000..982001fedbe
--- /dev/null
+++ b/db/post_migrate/20181105201455_steal_fill_store_upload.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class StealFillStoreUpload < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ BATCH_SIZE = 10_000
+
+ disable_ddl_transaction!
+
+ class Upload < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'uploads'
+ self.inheritance_column = :_type_disabled # Disable STI
+ end
+
+ def up
+ Gitlab::BackgroundMigration.steal('FillStoreUpload')
+
+ Upload.where(store: nil).each_batch(of: BATCH_SIZE) do |batch|
+ range = batch.pluck('MIN(id)', 'MAX(id)').first
+
+ Gitlab::BackgroundMigration::FillStoreUpload.new.perform(*range)
+ end
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/post_migrate/20181107054254_remove_restricted_todos_again.rb b/db/post_migrate/20181107054254_remove_restricted_todos_again.rb
new file mode 100644
index 00000000000..644e0074c46
--- /dev/null
+++ b/db/post_migrate/20181107054254_remove_restricted_todos_again.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+# rescheduling of the revised RemoveRestrictedTodosWithCte background migration
+class RemoveRestrictedTodosAgain < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ disable_ddl_transaction!
+
+ MIGRATION = 'RemoveRestrictedTodos'.freeze
+ BATCH_SIZE = 1000
+ DELAY_INTERVAL = 5.minutes.to_i
+
+ class Project < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'projects'
+ end
+
+ def up
+ Project.where('EXISTS (SELECT 1 FROM todos WHERE todos.project_id = projects.id)')
+ .each_batch(of: BATCH_SIZE) do |batch, index|
+ range = batch.pluck('MIN(id)', 'MAX(id)').first
+
+ BackgroundMigrationWorker.perform_in(index * DELAY_INTERVAL, MIGRATION, range)
+ end
+ end
+
+ def down
+ # nothing to do
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 6b0bb33f969..deaa2d30b26 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -1,4 +1,3 @@
-# encoding: UTF-8
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
@@ -11,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20181017001059) do
+ActiveRecord::Schema.define(version: 20181107054254) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -126,7 +125,7 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.boolean "unique_ips_limit_enabled", default: false, null: false
t.string "default_artifacts_expire_in", default: "0", null: false
t.string "uuid"
- t.decimal "polling_interval_multiplier", default: 1.0, null: false
+ t.decimal "polling_interval_multiplier", default: "1.0", null: false
t.integer "cached_markdown_version"
t.boolean "clientside_sentry_enabled", default: false, null: false
t.string "clientside_sentry_dsn"
@@ -165,6 +164,8 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.integer "usage_stats_set_by_user_id"
t.integer "receive_max_input_size"
t.integer "diff_max_patch_bytes", default: 102400, null: false
+ t.integer "archive_builds_in_seconds"
+ t.string "commit_email_hostname"
end
create_table "audit_events", force: :cascade do |t|
@@ -175,10 +176,9 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.text "details"
t.datetime "created_at"
t.datetime "updated_at"
+ t.index ["entity_id", "entity_type"], name: "index_audit_events_on_entity_id_and_entity_type", using: :btree
end
- add_index "audit_events", ["entity_id", "entity_type"], name: "index_audit_events_on_entity_id_and_entity_type", using: :btree
-
create_table "award_emoji", force: :cascade do |t|
t.string "name"
t.integer "user_id"
@@ -186,11 +186,10 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.string "awardable_type"
t.datetime "created_at"
t.datetime "updated_at"
+ t.index ["awardable_type", "awardable_id"], name: "index_award_emoji_on_awardable_type_and_awardable_id", using: :btree
+ t.index ["user_id", "name"], name: "index_award_emoji_on_user_id_and_name", using: :btree
end
- add_index "award_emoji", ["awardable_type", "awardable_id"], name: "index_award_emoji_on_awardable_type_and_awardable_id", using: :btree
- add_index "award_emoji", ["user_id", "name"], name: "index_award_emoji_on_user_id_and_name", using: :btree
-
create_table "badges", force: :cascade do |t|
t.string "link_url", null: false
t.string "image_url", null: false
@@ -199,47 +198,43 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.string "type", null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
+ t.index ["group_id"], name: "index_badges_on_group_id", using: :btree
+ t.index ["project_id"], name: "index_badges_on_project_id", using: :btree
end
- add_index "badges", ["group_id"], name: "index_badges_on_group_id", using: :btree
- add_index "badges", ["project_id"], name: "index_badges_on_project_id", using: :btree
-
create_table "board_group_recent_visits", id: :bigserial, force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.integer "user_id"
t.integer "board_id"
t.integer "group_id"
+ t.index ["board_id"], name: "index_board_group_recent_visits_on_board_id", using: :btree
+ t.index ["group_id"], name: "index_board_group_recent_visits_on_group_id", using: :btree
+ t.index ["user_id", "group_id", "board_id"], name: "index_board_group_recent_visits_on_user_group_and_board", unique: true, using: :btree
+ t.index ["user_id"], name: "index_board_group_recent_visits_on_user_id", using: :btree
end
- add_index "board_group_recent_visits", ["board_id"], name: "index_board_group_recent_visits_on_board_id", using: :btree
- add_index "board_group_recent_visits", ["group_id"], name: "index_board_group_recent_visits_on_group_id", using: :btree
- add_index "board_group_recent_visits", ["user_id", "group_id", "board_id"], name: "index_board_group_recent_visits_on_user_group_and_board", unique: true, using: :btree
- add_index "board_group_recent_visits", ["user_id"], name: "index_board_group_recent_visits_on_user_id", using: :btree
-
create_table "board_project_recent_visits", id: :bigserial, force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.integer "user_id"
t.integer "project_id"
t.integer "board_id"
+ t.index ["board_id"], name: "index_board_project_recent_visits_on_board_id", using: :btree
+ t.index ["project_id"], name: "index_board_project_recent_visits_on_project_id", using: :btree
+ t.index ["user_id", "project_id", "board_id"], name: "index_board_project_recent_visits_on_user_project_and_board", unique: true, using: :btree
+ t.index ["user_id"], name: "index_board_project_recent_visits_on_user_id", using: :btree
end
- add_index "board_project_recent_visits", ["board_id"], name: "index_board_project_recent_visits_on_board_id", using: :btree
- add_index "board_project_recent_visits", ["project_id"], name: "index_board_project_recent_visits_on_project_id", using: :btree
- add_index "board_project_recent_visits", ["user_id", "project_id", "board_id"], name: "index_board_project_recent_visits_on_user_project_and_board", unique: true, using: :btree
- add_index "board_project_recent_visits", ["user_id"], name: "index_board_project_recent_visits_on_user_id", using: :btree
-
create_table "boards", force: :cascade do |t|
t.integer "project_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "group_id"
+ t.index ["group_id"], name: "index_boards_on_group_id", using: :btree
+ t.index ["project_id"], name: "index_boards_on_project_id", using: :btree
end
- add_index "boards", ["group_id"], name: "index_boards_on_group_id", using: :btree
- add_index "boards", ["project_id"], name: "index_boards_on_project_id", using: :btree
-
create_table "broadcast_messages", force: :cascade do |t|
t.text "message", null: false
t.datetime "starts_at", null: false
@@ -250,10 +245,9 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.string "font"
t.text "message_html", null: false
t.integer "cached_markdown_version"
+ t.index ["starts_at", "ends_at", "id"], name: "index_broadcast_messages_on_starts_at_and_ends_at_and_id", using: :btree
end
- add_index "broadcast_messages", ["starts_at", "ends_at", "id"], name: "index_broadcast_messages_on_starts_at_and_ends_at_and_id", using: :btree
-
create_table "chat_names", force: :cascade do |t|
t.integer "user_id", null: false
t.integer "service_id", null: false
@@ -264,51 +258,46 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.datetime "last_used_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.index ["service_id", "team_id", "chat_id"], name: "index_chat_names_on_service_id_and_team_id_and_chat_id", unique: true, using: :btree
+ t.index ["user_id", "service_id"], name: "index_chat_names_on_user_id_and_service_id", unique: true, using: :btree
end
- add_index "chat_names", ["service_id", "team_id", "chat_id"], name: "index_chat_names_on_service_id_and_team_id_and_chat_id", unique: true, using: :btree
- add_index "chat_names", ["user_id", "service_id"], name: "index_chat_names_on_user_id_and_service_id", unique: true, using: :btree
-
create_table "chat_teams", force: :cascade do |t|
t.integer "namespace_id", null: false
t.string "team_id"
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.index ["namespace_id"], name: "index_chat_teams_on_namespace_id", unique: true, using: :btree
end
- add_index "chat_teams", ["namespace_id"], name: "index_chat_teams_on_namespace_id", unique: true, using: :btree
-
create_table "ci_build_trace_chunks", id: :bigserial, force: :cascade do |t|
t.integer "build_id", null: false
t.integer "chunk_index", null: false
t.integer "data_store", null: false
t.binary "raw_data"
+ t.index ["build_id", "chunk_index"], name: "index_ci_build_trace_chunks_on_build_id_and_chunk_index", unique: true, using: :btree
end
- add_index "ci_build_trace_chunks", ["build_id", "chunk_index"], name: "index_ci_build_trace_chunks_on_build_id_and_chunk_index", unique: true, using: :btree
-
create_table "ci_build_trace_section_names", 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
- add_index "ci_build_trace_section_names", ["project_id", "name"], name: "index_ci_build_trace_section_names_on_project_id_and_name", unique: true, using: :btree
-
create_table "ci_build_trace_sections", 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
- t.integer "byte_start", limit: 8, null: false
- t.integer "byte_end", limit: 8, null: false
+ t.bigint "byte_start", null: false
+ t.bigint "byte_end", null: false
t.integer "build_id", null: false
t.integer "section_name_id", null: false
+ t.index ["build_id", "section_name_id"], name: "index_ci_build_trace_sections_on_build_id_and_section_name_id", unique: true, using: :btree
+ t.index ["project_id"], name: "index_ci_build_trace_sections_on_project_id", using: :btree
+ t.index ["section_name_id"], name: "index_ci_build_trace_sections_on_section_name_id", using: :btree
end
- add_index "ci_build_trace_sections", ["build_id", "section_name_id"], name: "index_ci_build_trace_sections_on_build_id_and_section_name_id", unique: true, using: :btree
- add_index "ci_build_trace_sections", ["project_id"], name: "index_ci_build_trace_sections_on_project_id", using: :btree
- add_index "ci_build_trace_sections", ["section_name_id"], name: "index_ci_build_trace_sections_on_section_name_id", using: :btree
-
create_table "ci_builds", force: :cascade do |t|
t.string "status"
t.datetime "finished_at"
@@ -339,7 +328,7 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.datetime "erased_at"
t.datetime "artifacts_expire_at"
t.string "environment"
- t.integer "artifacts_size", limit: 8
+ t.bigint "artifacts_size"
t.string "when"
t.text "yaml_variables"
t.datetime "queued_at"
@@ -354,45 +343,42 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.boolean "protected"
t.integer "failure_reason"
t.datetime_with_timezone "scheduled_at"
+ t.index ["artifacts_expire_at"], name: "index_ci_builds_on_artifacts_expire_at", where: "(artifacts_file <> ''::text)", using: :btree
+ t.index ["auto_canceled_by_id"], name: "index_ci_builds_on_auto_canceled_by_id", using: :btree
+ t.index ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree
+ t.index ["commit_id", "status", "type"], name: "index_ci_builds_on_commit_id_and_status_and_type", using: :btree
+ t.index ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree
+ t.index ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree
+ t.index ["id"], name: "partial_index_ci_builds_on_id_with_legacy_artifacts", where: "(artifacts_file <> ''::text)", using: :btree
+ t.index ["project_id", "id"], name: "index_ci_builds_on_project_id_and_id", using: :btree
+ t.index ["protected"], name: "index_ci_builds_on_protected", using: :btree
+ t.index ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
+ t.index ["scheduled_at"], name: "partial_index_ci_builds_on_scheduled_at_with_scheduled_jobs", where: "((scheduled_at IS NOT NULL) AND ((type)::text = 'Ci::Build'::text) AND ((status)::text = 'scheduled'::text))", using: :btree
+ t.index ["stage_id", "stage_idx"], name: "tmp_build_stage_position_index", where: "(stage_idx IS NOT NULL)", using: :btree
+ t.index ["stage_id"], name: "index_ci_builds_on_stage_id", using: :btree
+ t.index ["status", "type", "runner_id"], name: "index_ci_builds_on_status_and_type_and_runner_id", using: :btree
+ t.index ["token"], name: "index_ci_builds_on_token", unique: true, using: :btree
+ t.index ["updated_at"], name: "index_ci_builds_on_updated_at", using: :btree
+ t.index ["user_id"], name: "index_ci_builds_on_user_id", using: :btree
end
- add_index "ci_builds", ["artifacts_expire_at"], name: "index_ci_builds_on_artifacts_expire_at", where: "(artifacts_file <> ''::text)", using: :btree
- add_index "ci_builds", ["auto_canceled_by_id"], name: "index_ci_builds_on_auto_canceled_by_id", using: :btree
- add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree
- add_index "ci_builds", ["commit_id", "status", "type"], name: "index_ci_builds_on_commit_id_and_status_and_type", using: :btree
- add_index "ci_builds", ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree
- add_index "ci_builds", ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree
- add_index "ci_builds", ["id"], name: "partial_index_ci_builds_on_id_with_legacy_artifacts", where: "(artifacts_file <> ''::text)", using: :btree
- add_index "ci_builds", ["project_id", "id"], name: "index_ci_builds_on_project_id_and_id", using: :btree
- add_index "ci_builds", ["protected"], name: "index_ci_builds_on_protected", using: :btree
- add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
- add_index "ci_builds", ["scheduled_at"], name: "partial_index_ci_builds_on_scheduled_at_with_scheduled_jobs", where: "((scheduled_at IS NOT NULL) AND ((type)::text = 'Ci::Build'::text) AND ((status)::text = 'scheduled'::text))", using: :btree
- add_index "ci_builds", ["stage_id", "stage_idx"], name: "tmp_build_stage_position_index", where: "(stage_idx IS NOT NULL)", using: :btree
- add_index "ci_builds", ["stage_id"], name: "index_ci_builds_on_stage_id", using: :btree
- add_index "ci_builds", ["status", "type", "runner_id"], name: "index_ci_builds_on_status_and_type_and_runner_id", using: :btree
- add_index "ci_builds", ["token"], name: "index_ci_builds_on_token", unique: true, using: :btree
- add_index "ci_builds", ["updated_at"], name: "index_ci_builds_on_updated_at", using: :btree
- add_index "ci_builds", ["user_id"], name: "index_ci_builds_on_user_id", using: :btree
-
create_table "ci_builds_metadata", force: :cascade do |t|
t.integer "build_id", null: false
t.integer "project_id", null: false
t.integer "timeout"
t.integer "timeout_source", default: 1, null: false
+ t.index ["build_id"], name: "index_ci_builds_metadata_on_build_id", unique: true, using: :btree
+ t.index ["project_id"], name: "index_ci_builds_metadata_on_project_id", using: :btree
end
- add_index "ci_builds_metadata", ["build_id"], name: "index_ci_builds_metadata_on_build_id", unique: true, using: :btree
- add_index "ci_builds_metadata", ["project_id"], name: "index_ci_builds_metadata_on_project_id", using: :btree
-
create_table "ci_builds_runner_session", id: :bigserial, force: :cascade do |t|
t.integer "build_id", null: false
t.string "url", null: false
t.string "certificate"
t.string "authorization"
+ t.index ["build_id"], name: "index_ci_builds_runner_session_on_build_id", unique: true, using: :btree
end
- add_index "ci_builds_runner_session", ["build_id"], name: "index_ci_builds_runner_session_on_build_id", unique: true, using: :btree
-
create_table "ci_group_variables", force: :cascade do |t|
t.string "key", null: false
t.text "value"
@@ -403,16 +389,15 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.boolean "protected", default: false, null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
+ t.index ["group_id", "key"], name: "index_ci_group_variables_on_group_id_and_key", unique: true, using: :btree
end
- add_index "ci_group_variables", ["group_id", "key"], name: "index_ci_group_variables_on_group_id_and_key", unique: true, using: :btree
-
create_table "ci_job_artifacts", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "job_id", null: false
t.integer "file_type", null: false
t.integer "file_store"
- t.integer "size", limit: 8
+ t.bigint "size"
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.datetime_with_timezone "expire_at"
@@ -420,13 +405,12 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.binary "file_sha256"
t.integer "file_format", limit: 2
t.integer "file_location", limit: 2
+ t.index ["expire_at", "job_id"], name: "index_ci_job_artifacts_on_expire_at_and_job_id", using: :btree
+ t.index ["file_store"], name: "index_ci_job_artifacts_on_file_store", using: :btree
+ t.index ["job_id", "file_type"], name: "index_ci_job_artifacts_on_job_id_and_file_type", unique: true, using: :btree
+ t.index ["project_id"], name: "index_ci_job_artifacts_on_project_id", using: :btree
end
- add_index "ci_job_artifacts", ["expire_at", "job_id"], name: "index_ci_job_artifacts_on_expire_at_and_job_id", using: :btree
- add_index "ci_job_artifacts", ["file_store"], name: "index_ci_job_artifacts_on_file_store", using: :btree
- add_index "ci_job_artifacts", ["job_id", "file_type"], name: "index_ci_job_artifacts_on_job_id_and_file_type", unique: true, using: :btree
- add_index "ci_job_artifacts", ["project_id"], name: "index_ci_job_artifacts_on_project_id", using: :btree
-
create_table "ci_pipeline_schedule_variables", force: :cascade do |t|
t.string "key", null: false
t.text "value"
@@ -436,10 +420,9 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.integer "pipeline_schedule_id", null: false
t.datetime_with_timezone "created_at"
t.datetime_with_timezone "updated_at"
+ t.index ["pipeline_schedule_id", "key"], name: "index_ci_pipeline_schedule_variables_on_schedule_id_and_key", unique: true, using: :btree
end
- add_index "ci_pipeline_schedule_variables", ["pipeline_schedule_id", "key"], name: "index_ci_pipeline_schedule_variables_on_schedule_id_and_key", unique: true, using: :btree
-
create_table "ci_pipeline_schedules", force: :cascade do |t|
t.string "description"
t.string "ref"
@@ -451,11 +434,10 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.boolean "active", default: true
t.datetime "created_at"
t.datetime "updated_at"
+ t.index ["next_run_at", "active"], name: "index_ci_pipeline_schedules_on_next_run_at_and_active", using: :btree
+ t.index ["project_id"], name: "index_ci_pipeline_schedules_on_project_id", using: :btree
end
- add_index "ci_pipeline_schedules", ["next_run_at", "active"], name: "index_ci_pipeline_schedules_on_next_run_at_and_active", using: :btree
- add_index "ci_pipeline_schedules", ["project_id"], name: "index_ci_pipeline_schedules_on_project_id", using: :btree
-
create_table "ci_pipeline_variables", force: :cascade do |t|
t.string "key", null: false
t.text "value"
@@ -463,10 +445,9 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.string "encrypted_value_salt"
t.string "encrypted_value_iv"
t.integer "pipeline_id", null: false
+ t.index ["pipeline_id", "key"], name: "index_ci_pipeline_variables_on_pipeline_id_and_key", unique: true, using: :btree
end
- add_index "ci_pipeline_variables", ["pipeline_id", "key"], name: "index_ci_pipeline_variables_on_pipeline_id_and_key", unique: true, using: :btree
-
create_table "ci_pipelines", force: :cascade do |t|
t.string "ref"
t.string "sha"
@@ -490,37 +471,34 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.boolean "protected"
t.integer "failure_reason"
t.integer "iid"
+ t.index ["auto_canceled_by_id"], name: "index_ci_pipelines_on_auto_canceled_by_id", using: :btree
+ t.index ["pipeline_schedule_id"], name: "index_ci_pipelines_on_pipeline_schedule_id", using: :btree
+ t.index ["project_id", "iid"], name: "index_ci_pipelines_on_project_id_and_iid", unique: true, where: "(iid IS NOT NULL)", using: :btree
+ t.index ["project_id", "ref", "status", "id"], name: "index_ci_pipelines_on_project_id_and_ref_and_status_and_id", using: :btree
+ t.index ["project_id", "sha"], name: "index_ci_pipelines_on_project_id_and_sha", using: :btree
+ t.index ["project_id", "source"], name: "index_ci_pipelines_on_project_id_and_source", using: :btree
+ t.index ["project_id", "status", "config_source"], name: "index_ci_pipelines_on_project_id_and_status_and_config_source", using: :btree
+ t.index ["project_id"], name: "index_ci_pipelines_on_project_id", using: :btree
+ t.index ["status"], name: "index_ci_pipelines_on_status", using: :btree
+ t.index ["user_id"], name: "index_ci_pipelines_on_user_id", using: :btree
end
- add_index "ci_pipelines", ["auto_canceled_by_id"], name: "index_ci_pipelines_on_auto_canceled_by_id", using: :btree
- add_index "ci_pipelines", ["pipeline_schedule_id"], name: "index_ci_pipelines_on_pipeline_schedule_id", using: :btree
- add_index "ci_pipelines", ["project_id", "iid"], name: "index_ci_pipelines_on_project_id_and_iid", unique: true, where: "(iid IS NOT NULL)", using: :btree
- add_index "ci_pipelines", ["project_id", "ref", "status", "id"], name: "index_ci_pipelines_on_project_id_and_ref_and_status_and_id", using: :btree
- add_index "ci_pipelines", ["project_id", "sha"], name: "index_ci_pipelines_on_project_id_and_sha", using: :btree
- add_index "ci_pipelines", ["project_id", "source"], name: "index_ci_pipelines_on_project_id_and_source", using: :btree
- add_index "ci_pipelines", ["project_id", "status", "config_source"], name: "index_ci_pipelines_on_project_id_and_status_and_config_source", using: :btree
- add_index "ci_pipelines", ["project_id"], name: "index_ci_pipelines_on_project_id", using: :btree
- add_index "ci_pipelines", ["status"], name: "index_ci_pipelines_on_status", using: :btree
- add_index "ci_pipelines", ["user_id"], name: "index_ci_pipelines_on_user_id", using: :btree
-
create_table "ci_runner_namespaces", 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
- add_index "ci_runner_namespaces", ["namespace_id"], name: "index_ci_runner_namespaces_on_namespace_id", using: :btree
- add_index "ci_runner_namespaces", ["runner_id", "namespace_id"], name: "index_ci_runner_namespaces_on_runner_id_and_namespace_id", unique: true, using: :btree
-
create_table "ci_runner_projects", force: :cascade do |t|
t.integer "runner_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
t.integer "project_id"
+ t.index ["project_id"], name: "index_ci_runner_projects_on_project_id", using: :btree
+ t.index ["runner_id"], name: "index_ci_runner_projects_on_runner_id", using: :btree
end
- add_index "ci_runner_projects", ["project_id"], name: "index_ci_runner_projects_on_project_id", using: :btree
- add_index "ci_runner_projects", ["runner_id"], name: "index_ci_runner_projects_on_runner_id", using: :btree
-
create_table "ci_runners", force: :cascade do |t|
t.string "token"
t.datetime "created_at"
@@ -540,14 +518,13 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.string "ip_address"
t.integer "maximum_timeout"
t.integer "runner_type", limit: 2, null: false
+ t.index ["contacted_at"], name: "index_ci_runners_on_contacted_at", using: :btree
+ t.index ["is_shared"], name: "index_ci_runners_on_is_shared", using: :btree
+ t.index ["locked"], name: "index_ci_runners_on_locked", using: :btree
+ t.index ["runner_type"], name: "index_ci_runners_on_runner_type", using: :btree
+ t.index ["token"], name: "index_ci_runners_on_token", using: :btree
end
- add_index "ci_runners", ["contacted_at"], name: "index_ci_runners_on_contacted_at", using: :btree
- add_index "ci_runners", ["is_shared"], name: "index_ci_runners_on_is_shared", using: :btree
- add_index "ci_runners", ["locked"], name: "index_ci_runners_on_locked", using: :btree
- add_index "ci_runners", ["runner_type"], name: "index_ci_runners_on_runner_type", using: :btree
- add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree
-
create_table "ci_stages", force: :cascade do |t|
t.integer "project_id"
t.integer "pipeline_id"
@@ -557,23 +534,21 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.integer "status"
t.integer "lock_version"
t.integer "position"
+ t.index ["pipeline_id", "name"], name: "index_ci_stages_on_pipeline_id_and_name", unique: true, using: :btree
+ t.index ["pipeline_id", "position"], name: "index_ci_stages_on_pipeline_id_and_position", using: :btree
+ t.index ["pipeline_id"], name: "index_ci_stages_on_pipeline_id", using: :btree
+ t.index ["project_id"], name: "index_ci_stages_on_project_id", using: :btree
end
- add_index "ci_stages", ["pipeline_id", "name"], name: "index_ci_stages_on_pipeline_id_and_name", unique: true, using: :btree
- add_index "ci_stages", ["pipeline_id", "position"], name: "index_ci_stages_on_pipeline_id_and_position", using: :btree
- add_index "ci_stages", ["pipeline_id"], name: "index_ci_stages_on_pipeline_id", using: :btree
- add_index "ci_stages", ["project_id"], name: "index_ci_stages_on_project_id", using: :btree
-
create_table "ci_trigger_requests", force: :cascade do |t|
t.integer "trigger_id", null: false
t.text "variables"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "commit_id"
+ t.index ["commit_id"], name: "index_ci_trigger_requests_on_commit_id", using: :btree
end
- add_index "ci_trigger_requests", ["commit_id"], name: "index_ci_trigger_requests_on_commit_id", using: :btree
-
create_table "ci_triggers", force: :cascade do |t|
t.string "token"
t.datetime "created_at"
@@ -582,10 +557,9 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.integer "owner_id"
t.string "description"
t.string "ref"
+ t.index ["project_id"], name: "index_ci_triggers_on_project_id", using: :btree
end
- add_index "ci_triggers", ["project_id"], name: "index_ci_triggers_on_project_id", using: :btree
-
create_table "ci_variables", force: :cascade do |t|
t.string "key", null: false
t.text "value"
@@ -595,18 +569,16 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.integer "project_id", null: false
t.boolean "protected", default: false, null: false
t.string "environment_scope", default: "*", 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
- add_index "ci_variables", ["project_id", "key", "environment_scope"], name: "index_ci_variables_on_project_id_and_key_and_environment_scope", unique: true, using: :btree
-
create_table "cluster_groups", 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
- add_index "cluster_groups", ["cluster_id", "group_id"], name: "index_cluster_groups_on_cluster_id_and_group_id", unique: true, using: :btree
- add_index "cluster_groups", ["group_id"], name: "index_cluster_groups_on_group_id", using: :btree
-
create_table "cluster_platforms_kubernetes", force: :cascade do |t|
t.integer "cluster_id", null: false
t.datetime_with_timezone "created_at", null: false
@@ -620,20 +592,18 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.text "encrypted_token"
t.string "encrypted_token_iv"
t.integer "authorization_type", limit: 2
+ t.index ["cluster_id"], name: "index_cluster_platforms_kubernetes_on_cluster_id", unique: true, using: :btree
end
- add_index "cluster_platforms_kubernetes", ["cluster_id"], name: "index_cluster_platforms_kubernetes_on_cluster_id", unique: true, using: :btree
-
create_table "cluster_projects", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "cluster_id", null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
+ t.index ["cluster_id"], name: "index_cluster_projects_on_cluster_id", using: :btree
+ t.index ["project_id"], name: "index_cluster_projects_on_project_id", using: :btree
end
- add_index "cluster_projects", ["cluster_id"], name: "index_cluster_projects_on_cluster_id", using: :btree
- add_index "cluster_projects", ["project_id"], name: "index_cluster_projects_on_project_id", using: :btree
-
create_table "cluster_providers_gcp", force: :cascade do |t|
t.integer "cluster_id", null: false
t.integer "status"
@@ -649,10 +619,9 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.text "encrypted_access_token"
t.string "encrypted_access_token_iv"
t.boolean "legacy_abac", default: true, null: false
+ t.index ["cluster_id"], name: "index_cluster_providers_gcp_on_cluster_id", unique: true, using: :btree
end
- add_index "cluster_providers_gcp", ["cluster_id"], name: "index_cluster_providers_gcp_on_cluster_id", unique: true, using: :btree
-
create_table "clusters", force: :cascade do |t|
t.integer "user_id"
t.integer "provider_type"
@@ -663,11 +632,10 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.string "name", null: false
t.string "environment_scope", default: "*", null: false
t.integer "cluster_type", limit: 2, default: 3, null: false
+ t.index ["enabled"], name: "index_clusters_on_enabled", using: :btree
+ t.index ["user_id"], name: "index_clusters_on_user_id", using: :btree
end
- add_index "clusters", ["enabled"], name: "index_clusters_on_enabled", using: :btree
- add_index "clusters", ["user_id"], name: "index_clusters_on_user_id", using: :btree
-
create_table "clusters_applications_helm", force: :cascade do |t|
t.integer "cluster_id", null: false
t.datetime_with_timezone "created_at", null: false
@@ -703,6 +671,16 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.text "status_reason"
end
+ create_table "clusters_applications_knative", 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
+ t.integer "status", null: false
+ t.string "version", null: false
+ t.string "hostname"
+ t.text "status_reason"
+ end
+
create_table "clusters_applications_prometheus", force: :cascade do |t|
t.integer "cluster_id", null: false
t.integer "status", null: false
@@ -721,11 +699,10 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.string "version", null: false
t.text "status_reason"
t.boolean "privileged", default: true, null: false
+ t.index ["cluster_id"], name: "index_clusters_applications_runners_on_cluster_id", unique: true, using: :btree
+ t.index ["runner_id"], name: "index_clusters_applications_runners_on_runner_id", using: :btree
end
- add_index "clusters_applications_runners", ["cluster_id"], name: "index_clusters_applications_runners_on_cluster_id", unique: true, using: :btree
- add_index "clusters_applications_runners", ["runner_id"], name: "index_clusters_applications_runners_on_runner_id", using: :btree
-
create_table "clusters_kubernetes_namespaces", id: :bigserial, force: :cascade do |t|
t.integer "cluster_id", null: false
t.integer "project_id"
@@ -736,23 +713,21 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.string "encrypted_service_account_token_iv"
t.string "namespace", null: false
t.string "service_account_name"
+ t.index ["cluster_id", "namespace"], name: "kubernetes_namespaces_cluster_and_namespace", unique: true, using: :btree
+ t.index ["cluster_id"], name: "index_clusters_kubernetes_namespaces_on_cluster_id", using: :btree
+ t.index ["cluster_project_id"], name: "index_clusters_kubernetes_namespaces_on_cluster_project_id", using: :btree
+ t.index ["project_id"], name: "index_clusters_kubernetes_namespaces_on_project_id", using: :btree
end
- add_index "clusters_kubernetes_namespaces", ["cluster_id", "namespace"], name: "kubernetes_namespaces_cluster_and_namespace", unique: true, using: :btree
- add_index "clusters_kubernetes_namespaces", ["cluster_id"], name: "index_clusters_kubernetes_namespaces_on_cluster_id", using: :btree
- add_index "clusters_kubernetes_namespaces", ["cluster_project_id"], name: "index_clusters_kubernetes_namespaces_on_cluster_project_id", using: :btree
- add_index "clusters_kubernetes_namespaces", ["project_id"], name: "index_clusters_kubernetes_namespaces_on_project_id", using: :btree
-
create_table "container_repositories", force: :cascade do |t|
t.integer "project_id", null: false
t.string "name", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.index ["project_id", "name"], name: "index_container_repositories_on_project_id_and_name", unique: true, using: :btree
+ t.index ["project_id"], name: "index_container_repositories_on_project_id", using: :btree
end
- add_index "container_repositories", ["project_id", "name"], name: "index_container_repositories_on_project_id_and_name", unique: true, using: :btree
- add_index "container_repositories", ["project_id"], name: "index_container_repositories_on_project_id", using: :btree
-
create_table "conversational_development_index_metrics", force: :cascade do |t|
t.float "leader_issues", null: false
t.float "instance_issues", null: false
@@ -794,10 +769,9 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.datetime "created_at"
t.datetime "updated_at"
t.boolean "can_push", default: false, null: false
+ t.index ["project_id"], name: "index_deploy_keys_projects_on_project_id", using: :btree
end
- add_index "deploy_keys_projects", ["project_id"], name: "index_deploy_keys_projects_on_project_id", using: :btree
-
create_table "deploy_tokens", force: :cascade do |t|
t.boolean "revoked", default: false
t.boolean "read_repository", default: false, null: false
@@ -806,11 +780,10 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.datetime_with_timezone "created_at", null: false
t.string "name", null: false
t.string "token", null: false
+ t.index ["token", "expires_at", "id"], name: "index_deploy_tokens_on_token_and_expires_at_and_id", where: "(revoked IS FALSE)", using: :btree
+ t.index ["token"], name: "index_deploy_tokens_on_token", unique: true, using: :btree
end
- add_index "deploy_tokens", ["token", "expires_at", "id"], name: "index_deploy_tokens_on_token_and_expires_at_and_id", where: "(revoked IS FALSE)", using: :btree
- add_index "deploy_tokens", ["token"], name: "index_deploy_tokens_on_token", unique: true, using: :btree
-
create_table "deployments", force: :cascade do |t|
t.integer "iid", null: false
t.integer "project_id", null: false
@@ -824,14 +797,19 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.datetime "created_at"
t.datetime "updated_at"
t.string "on_stop"
+ t.integer "status", limit: 2, default: 2, null: false
+ t.datetime_with_timezone "finished_at"
+ t.index ["created_at"], name: "index_deployments_on_created_at", using: :btree
+ t.index ["deployable_type", "deployable_id"], name: "index_deployments_on_deployable_type_and_deployable_id", using: :btree
+ t.index ["environment_id", "id"], name: "index_deployments_on_environment_id_and_id", using: :btree
+ t.index ["environment_id", "iid", "project_id"], name: "index_deployments_on_environment_id_and_iid_and_project_id", using: :btree
+ t.index ["environment_id", "status"], name: "index_deployments_on_environment_id_and_status", using: :btree
+ t.index ["id"], name: "partial_index_deployments_for_legacy_successful_deployments", where: "((finished_at IS NULL) AND (status = 2))", using: :btree
+ t.index ["project_id", "iid"], name: "index_deployments_on_project_id_and_iid", unique: true, using: :btree
+ t.index ["project_id", "status", "created_at"], name: "index_deployments_on_project_id_and_status_and_created_at", using: :btree
+ t.index ["project_id", "status"], name: "index_deployments_on_project_id_and_status", using: :btree
end
- add_index "deployments", ["created_at"], name: "index_deployments_on_created_at", using: :btree
- add_index "deployments", ["deployable_type", "deployable_id"], name: "index_deployments_on_deployable_type_and_deployable_id", using: :btree
- add_index "deployments", ["environment_id", "id"], name: "index_deployments_on_environment_id_and_id", using: :btree
- add_index "deployments", ["environment_id", "iid", "project_id"], name: "index_deployments_on_environment_id_and_iid_and_project_id", using: :btree
- add_index "deployments", ["project_id", "iid"], name: "index_deployments_on_project_id_and_iid", unique: true, using: :btree
-
create_table "emails", force: :cascade do |t|
t.integer "user_id", null: false
t.string "email", null: false
@@ -840,12 +818,11 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.string "confirmation_token"
t.datetime_with_timezone "confirmed_at"
t.datetime_with_timezone "confirmation_sent_at"
+ t.index ["confirmation_token"], name: "index_emails_on_confirmation_token", unique: true, using: :btree
+ t.index ["email"], name: "index_emails_on_email", unique: true, using: :btree
+ t.index ["user_id"], name: "index_emails_on_user_id", using: :btree
end
- add_index "emails", ["confirmation_token"], name: "index_emails_on_confirmation_token", unique: true, using: :btree
- add_index "emails", ["email"], name: "index_emails_on_email", unique: true, using: :btree
- add_index "emails", ["user_id"], name: "index_emails_on_user_id", using: :btree
-
create_table "environments", force: :cascade do |t|
t.integer "project_id", null: false
t.string "name", null: false
@@ -855,11 +832,10 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.string "environment_type"
t.string "state", default: "available", null: false
t.string "slug", null: false
+ t.index ["project_id", "name"], name: "index_environments_on_project_id_and_name", unique: true, using: :btree
+ t.index ["project_id", "slug"], name: "index_environments_on_project_id_and_slug", unique: true, using: :btree
end
- add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", unique: true, using: :btree
- add_index "environments", ["project_id", "slug"], name: "index_environments_on_project_id_and_slug", unique: true, using: :btree
-
create_table "events", force: :cascade do |t|
t.integer "project_id"
t.integer "author_id", null: false
@@ -868,95 +844,59 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.datetime_with_timezone "updated_at", null: false
t.integer "action", limit: 2, null: false
t.string "target_type"
+ t.index ["action"], name: "index_events_on_action", using: :btree
+ t.index ["author_id", "project_id"], name: "index_events_on_author_id_and_project_id", using: :btree
+ t.index ["project_id", "id"], name: "index_events_on_project_id_and_id", using: :btree
+ t.index ["target_type", "target_id"], name: "index_events_on_target_type_and_target_id", using: :btree
end
- add_index "events", ["action"], name: "index_events_on_action", using: :btree
- add_index "events", ["author_id", "project_id"], name: "index_events_on_author_id_and_project_id", using: :btree
- add_index "events", ["project_id", "id"], name: "index_events_on_project_id_and_id", using: :btree
- add_index "events", ["target_type", "target_id"], name: "index_events_on_target_type_and_target_id", using: :btree
-
create_table "feature_gates", force: :cascade do |t|
t.string "feature_key", null: false
t.string "key", null: false
t.string "value"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.index ["feature_key", "key", "value"], name: "index_feature_gates_on_feature_key_and_key_and_value", unique: true, using: :btree
end
- add_index "feature_gates", ["feature_key", "key", "value"], name: "index_feature_gates_on_feature_key_and_key_and_value", unique: true, using: :btree
-
create_table "features", 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
- add_index "features", ["key"], name: "index_features_on_key", unique: true, using: :btree
-
create_table "fork_network_members", force: :cascade do |t|
t.integer "fork_network_id", null: false
t.integer "project_id", null: false
t.integer "forked_from_project_id"
+ t.index ["fork_network_id"], name: "index_fork_network_members_on_fork_network_id", using: :btree
+ t.index ["project_id"], name: "index_fork_network_members_on_project_id", unique: true, using: :btree
end
- add_index "fork_network_members", ["fork_network_id"], name: "index_fork_network_members_on_fork_network_id", using: :btree
- add_index "fork_network_members", ["project_id"], name: "index_fork_network_members_on_project_id", unique: true, using: :btree
-
create_table "fork_networks", 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
- add_index "fork_networks", ["root_project_id"], name: "index_fork_networks_on_root_project_id", unique: true, using: :btree
-
create_table "forked_project_links", force: :cascade do |t|
t.integer "forked_to_project_id", null: false
t.integer "forked_from_project_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
+ t.index ["forked_to_project_id"], name: "index_forked_project_links_on_forked_to_project_id", unique: true, using: :btree
end
- add_index "forked_project_links", ["forked_to_project_id"], name: "index_forked_project_links_on_forked_to_project_id", unique: true, using: :btree
-
- create_table "gcp_clusters", force: :cascade do |t|
- t.integer "project_id", null: false
- t.integer "user_id"
- t.integer "service_id"
- t.integer "status"
- t.integer "gcp_cluster_size", null: false
- t.datetime_with_timezone "created_at", null: false
- t.datetime_with_timezone "updated_at", null: false
- t.boolean "enabled", default: true
- t.text "status_reason"
- t.string "project_namespace"
- t.string "endpoint"
- t.text "ca_cert"
- t.text "encrypted_kubernetes_token"
- t.string "encrypted_kubernetes_token_iv"
- t.string "username"
- t.text "encrypted_password"
- t.string "encrypted_password_iv"
- t.string "gcp_project_id", null: false
- t.string "gcp_cluster_zone", null: false
- t.string "gcp_cluster_name", null: false
- t.string "gcp_machine_type"
- t.string "gcp_operation_id"
- t.text "encrypted_gcp_token"
- t.string "encrypted_gcp_token_iv"
- end
-
- add_index "gcp_clusters", ["project_id"], name: "index_gcp_clusters_on_project_id", unique: true, using: :btree
-
create_table "gpg_key_subkeys", force: :cascade do |t|
t.integer "gpg_key_id", null: false
t.binary "keyid"
t.binary "fingerprint"
+ t.index ["fingerprint"], name: "index_gpg_key_subkeys_on_fingerprint", unique: true, using: :btree
+ t.index ["gpg_key_id"], name: "index_gpg_key_subkeys_on_gpg_key_id", using: :btree
+ t.index ["keyid"], name: "index_gpg_key_subkeys_on_keyid", unique: true, using: :btree
end
- add_index "gpg_key_subkeys", ["fingerprint"], name: "index_gpg_key_subkeys_on_fingerprint", unique: true, using: :btree
- add_index "gpg_key_subkeys", ["gpg_key_id"], name: "index_gpg_key_subkeys_on_gpg_key_id", using: :btree
- add_index "gpg_key_subkeys", ["keyid"], name: "index_gpg_key_subkeys_on_keyid", unique: true, using: :btree
-
create_table "gpg_keys", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
@@ -964,12 +904,11 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.binary "primary_keyid"
t.binary "fingerprint"
t.text "key"
+ t.index ["fingerprint"], name: "index_gpg_keys_on_fingerprint", unique: true, using: :btree
+ t.index ["primary_keyid"], name: "index_gpg_keys_on_primary_keyid", unique: true, using: :btree
+ t.index ["user_id"], name: "index_gpg_keys_on_user_id", using: :btree
end
- add_index "gpg_keys", ["fingerprint"], name: "index_gpg_keys_on_fingerprint", unique: true, using: :btree
- add_index "gpg_keys", ["primary_keyid"], name: "index_gpg_keys_on_primary_keyid", unique: true, using: :btree
- add_index "gpg_keys", ["user_id"], name: "index_gpg_keys_on_user_id", using: :btree
-
create_table "gpg_signatures", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
@@ -981,63 +920,57 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.text "gpg_key_user_email"
t.integer "verification_status", limit: 2, default: 0, null: false
t.integer "gpg_key_subkey_id"
+ t.index ["commit_sha"], name: "index_gpg_signatures_on_commit_sha", unique: true, using: :btree
+ t.index ["gpg_key_id"], name: "index_gpg_signatures_on_gpg_key_id", using: :btree
+ t.index ["gpg_key_primary_keyid"], name: "index_gpg_signatures_on_gpg_key_primary_keyid", using: :btree
+ t.index ["gpg_key_subkey_id"], name: "index_gpg_signatures_on_gpg_key_subkey_id", using: :btree
+ t.index ["project_id"], name: "index_gpg_signatures_on_project_id", using: :btree
end
- add_index "gpg_signatures", ["commit_sha"], name: "index_gpg_signatures_on_commit_sha", unique: true, using: :btree
- add_index "gpg_signatures", ["gpg_key_id"], name: "index_gpg_signatures_on_gpg_key_id", using: :btree
- add_index "gpg_signatures", ["gpg_key_primary_keyid"], name: "index_gpg_signatures_on_gpg_key_primary_keyid", using: :btree
- add_index "gpg_signatures", ["gpg_key_subkey_id"], name: "index_gpg_signatures_on_gpg_key_subkey_id", using: :btree
- add_index "gpg_signatures", ["project_id"], name: "index_gpg_signatures_on_project_id", using: :btree
-
create_table "group_custom_attributes", 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
t.string "key", null: false
t.string "value", null: false
+ t.index ["group_id", "key"], name: "index_group_custom_attributes_on_group_id_and_key", unique: true, using: :btree
+ t.index ["key", "value"], name: "index_group_custom_attributes_on_key_and_value", using: :btree
end
- add_index "group_custom_attributes", ["group_id", "key"], name: "index_group_custom_attributes_on_group_id_and_key", unique: true, using: :btree
- add_index "group_custom_attributes", ["key", "value"], name: "index_group_custom_attributes_on_key_and_value", using: :btree
-
create_table "identities", force: :cascade do |t|
t.string "extern_uid"
t.string "provider"
t.integer "user_id"
t.datetime "created_at"
t.datetime "updated_at"
+ t.index ["user_id"], name: "index_identities_on_user_id", using: :btree
end
- add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree
-
create_table "import_export_uploads", force: :cascade do |t|
t.datetime_with_timezone "updated_at", null: false
t.integer "project_id"
t.text "import_file"
t.text "export_file"
+ t.index ["project_id"], name: "index_import_export_uploads_on_project_id", using: :btree
+ t.index ["updated_at"], name: "index_import_export_uploads_on_updated_at", using: :btree
end
- add_index "import_export_uploads", ["project_id"], name: "index_import_export_uploads_on_project_id", using: :btree
- add_index "import_export_uploads", ["updated_at"], name: "index_import_export_uploads_on_updated_at", using: :btree
-
create_table "internal_ids", id: :bigserial, force: :cascade do |t|
t.integer "project_id"
t.integer "usage", null: false
t.integer "last_value", null: false
t.integer "namespace_id"
+ t.index ["usage", "namespace_id"], name: "index_internal_ids_on_usage_and_namespace_id", unique: true, where: "(namespace_id IS NOT NULL)", using: :btree
+ t.index ["usage", "project_id"], name: "index_internal_ids_on_usage_and_project_id", unique: true, where: "(project_id IS NOT NULL)", using: :btree
end
- add_index "internal_ids", ["usage", "namespace_id"], name: "index_internal_ids_on_usage_and_namespace_id", unique: true, where: "(namespace_id IS NOT NULL)", using: :btree
- add_index "internal_ids", ["usage", "project_id"], name: "index_internal_ids_on_usage_and_project_id", unique: true, where: "(project_id IS NOT NULL)", using: :btree
-
create_table "issue_assignees", id: false, force: :cascade do |t|
t.integer "user_id", null: false
t.integer "issue_id", null: false
+ t.index ["issue_id", "user_id"], name: "index_issue_assignees_on_issue_id_and_user_id", unique: true, using: :btree
+ t.index ["user_id"], name: "index_issue_assignees_on_user_id", using: :btree
end
- add_index "issue_assignees", ["issue_id", "user_id"], name: "index_issue_assignees_on_issue_id_and_user_id", unique: true, using: :btree
- add_index "issue_assignees", ["user_id"], name: "index_issue_assignees_on_user_id", using: :btree
-
create_table "issue_metrics", force: :cascade do |t|
t.integer "issue_id", null: false
t.datetime "first_mentioned_in_commit_at"
@@ -1045,10 +978,9 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.datetime "first_added_to_board_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.index ["issue_id"], name: "index_issue_metrics", using: :btree
end
- add_index "issue_metrics", ["issue_id"], name: "index_issue_metrics", using: :btree
-
create_table "issues", force: :cascade do |t|
t.string "title"
t.integer "author_id"
@@ -1074,23 +1006,22 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.boolean "discussion_locked"
t.datetime_with_timezone "closed_at"
t.integer "closed_by_id"
+ t.index ["author_id"], name: "index_issues_on_author_id", using: :btree
+ t.index ["confidential"], name: "index_issues_on_confidential", using: :btree
+ t.index ["description"], name: "index_issues_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
+ t.index ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree
+ t.index ["moved_to_id"], name: "index_issues_on_moved_to_id", where: "(moved_to_id IS NOT NULL)", using: :btree
+ t.index ["project_id", "created_at", "id", "state"], name: "index_issues_on_project_id_and_created_at_and_id_and_state", using: :btree
+ t.index ["project_id", "due_date", "id", "state"], name: "idx_issues_on_project_id_and_due_date_and_id_and_state_partial", where: "(due_date IS NOT NULL)", using: :btree
+ t.index ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree
+ t.index ["project_id", "updated_at", "id", "state"], name: "index_issues_on_project_id_and_updated_at_and_id_and_state", using: :btree
+ t.index ["relative_position"], name: "index_issues_on_relative_position", using: :btree
+ t.index ["state"], name: "index_issues_on_state", using: :btree
+ t.index ["title"], name: "index_issues_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
+ t.index ["updated_at"], name: "index_issues_on_updated_at", using: :btree
+ t.index ["updated_by_id"], name: "index_issues_on_updated_by_id", where: "(updated_by_id IS NOT NULL)", using: :btree
end
- add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree
- add_index "issues", ["confidential"], name: "index_issues_on_confidential", using: :btree
- add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
- add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree
- add_index "issues", ["moved_to_id"], name: "index_issues_on_moved_to_id", where: "(moved_to_id IS NOT NULL)", using: :btree
- add_index "issues", ["project_id", "created_at", "id", "state"], name: "index_issues_on_project_id_and_created_at_and_id_and_state", using: :btree
- add_index "issues", ["project_id", "due_date", "id", "state"], name: "idx_issues_on_project_id_and_due_date_and_id_and_state_partial", where: "(due_date IS NOT NULL)", using: :btree
- add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree
- add_index "issues", ["project_id", "updated_at", "id", "state"], name: "index_issues_on_project_id_and_updated_at_and_id_and_state", using: :btree
- add_index "issues", ["relative_position"], name: "index_issues_on_relative_position", using: :btree
- add_index "issues", ["state"], name: "index_issues_on_state", using: :btree
- add_index "issues", ["title"], name: "index_issues_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
- add_index "issues", ["updated_at"], name: "index_issues_on_updated_at", using: :btree
- add_index "issues", ["updated_by_id"], name: "index_issues_on_updated_by_id", where: "(updated_by_id IS NOT NULL)", using: :btree
-
create_table "keys", force: :cascade do |t|
t.integer "user_id"
t.datetime "created_at"
@@ -1101,33 +1032,30 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.string "fingerprint"
t.boolean "public", default: false, null: false
t.datetime "last_used_at"
+ t.index ["fingerprint"], name: "index_keys_on_fingerprint", unique: true, using: :btree
+ t.index ["user_id"], name: "index_keys_on_user_id", using: :btree
end
- add_index "keys", ["fingerprint"], name: "index_keys_on_fingerprint", unique: true, using: :btree
- add_index "keys", ["user_id"], name: "index_keys_on_user_id", using: :btree
-
create_table "label_links", force: :cascade do |t|
t.integer "label_id"
t.integer "target_id"
t.string "target_type"
t.datetime "created_at"
t.datetime "updated_at"
+ t.index ["label_id"], name: "index_label_links_on_label_id", using: :btree
+ t.index ["target_id", "target_type"], name: "index_label_links_on_target_id_and_target_type", using: :btree
end
- add_index "label_links", ["label_id"], name: "index_label_links_on_label_id", using: :btree
- add_index "label_links", ["target_id", "target_type"], name: "index_label_links_on_target_id_and_target_type", using: :btree
-
create_table "label_priorities", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "label_id", null: false
t.integer "priority", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.index ["priority"], name: "index_label_priorities_on_priority", using: :btree
+ t.index ["project_id", "label_id"], name: "index_label_priorities_on_project_id_and_label_id", unique: true, using: :btree
end
- add_index "label_priorities", ["priority"], name: "index_label_priorities_on_priority", using: :btree
- add_index "label_priorities", ["project_id", "label_id"], name: "index_label_priorities_on_project_id_and_label_id", unique: true, using: :btree
-
create_table "labels", force: :cascade do |t|
t.string "title"
t.string "color"
@@ -1140,45 +1068,41 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.string "type"
t.integer "group_id"
t.integer "cached_markdown_version"
+ t.index ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree
+ t.index ["project_id"], name: "index_labels_on_project_id", using: :btree
+ t.index ["template"], name: "index_labels_on_template", where: "template", using: :btree
+ t.index ["title"], name: "index_labels_on_title", using: :btree
+ t.index ["type", "project_id"], name: "index_labels_on_type_and_project_id", using: :btree
end
- add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree
- add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree
- add_index "labels", ["template"], name: "index_labels_on_template", where: "template", using: :btree
- add_index "labels", ["title"], name: "index_labels_on_title", using: :btree
- add_index "labels", ["type", "project_id"], name: "index_labels_on_type_and_project_id", using: :btree
-
create_table "lfs_file_locks", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "user_id", null: false
t.datetime "created_at", null: false
t.string "path", limit: 511
+ t.index ["project_id", "path"], name: "index_lfs_file_locks_on_project_id_and_path", unique: true, using: :btree
+ t.index ["user_id"], name: "index_lfs_file_locks_on_user_id", using: :btree
end
- add_index "lfs_file_locks", ["project_id", "path"], name: "index_lfs_file_locks_on_project_id_and_path", unique: true, using: :btree
- add_index "lfs_file_locks", ["user_id"], name: "index_lfs_file_locks_on_user_id", using: :btree
-
create_table "lfs_objects", force: :cascade do |t|
t.string "oid", null: false
- t.integer "size", limit: 8, null: false
+ t.bigint "size", null: false
t.datetime "created_at"
t.datetime "updated_at"
t.string "file"
t.integer "file_store"
+ t.index ["file_store"], name: "index_lfs_objects_on_file_store", using: :btree
+ t.index ["oid"], name: "index_lfs_objects_on_oid", unique: true, using: :btree
end
- add_index "lfs_objects", ["file_store"], name: "index_lfs_objects_on_file_store", using: :btree
- add_index "lfs_objects", ["oid"], name: "index_lfs_objects_on_oid", unique: true, using: :btree
-
create_table "lfs_objects_projects", force: :cascade do |t|
t.integer "lfs_object_id", null: false
t.integer "project_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
+ t.index ["project_id"], name: "index_lfs_objects_projects_on_project_id", using: :btree
end
- add_index "lfs_objects_projects", ["project_id"], name: "index_lfs_objects_projects_on_project_id", using: :btree
-
create_table "lists", force: :cascade do |t|
t.integer "board_id", null: false
t.integer "label_id"
@@ -1186,12 +1110,11 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.integer "position"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.index ["board_id", "label_id"], name: "index_lists_on_board_id_and_label_id", unique: true, using: :btree
+ t.index ["label_id"], name: "index_lists_on_label_id", using: :btree
+ t.index ["list_type"], name: "index_lists_on_list_type", using: :btree
end
- add_index "lists", ["board_id", "label_id"], name: "index_lists_on_board_id_and_label_id", unique: true, using: :btree
- add_index "lists", ["label_id"], name: "index_lists_on_label_id", using: :btree
- add_index "lists", ["list_type"], name: "index_lists_on_list_type", using: :btree
-
create_table "members", force: :cascade do |t|
t.integer "access_level", null: false
t.integer "source_id", null: false
@@ -1207,14 +1130,13 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.datetime "invite_accepted_at"
t.datetime "requested_at"
t.date "expires_at"
+ t.index ["access_level"], name: "index_members_on_access_level", using: :btree
+ t.index ["invite_token"], name: "index_members_on_invite_token", unique: true, using: :btree
+ t.index ["requested_at"], name: "index_members_on_requested_at", using: :btree
+ t.index ["source_id", "source_type"], name: "index_members_on_source_id_and_source_type", using: :btree
+ t.index ["user_id"], name: "index_members_on_user_id", using: :btree
end
- add_index "members", ["access_level"], name: "index_members_on_access_level", using: :btree
- add_index "members", ["invite_token"], name: "index_members_on_invite_token", unique: true, using: :btree
- add_index "members", ["requested_at"], name: "index_members_on_requested_at", using: :btree
- add_index "members", ["source_id", "source_type"], name: "index_members_on_source_id_and_source_type", using: :btree
- add_index "members", ["user_id"], name: "index_members_on_user_id", using: :btree
-
create_table "merge_request_diff_commits", id: false, force: :cascade do |t|
t.datetime_with_timezone "authored_date"
t.datetime_with_timezone "committed_date"
@@ -1226,11 +1148,10 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.text "committer_name"
t.text "committer_email"
t.text "message"
+ t.index ["merge_request_diff_id", "relative_order"], name: "index_merge_request_diff_commits_on_mr_diff_id_and_order", unique: true, using: :btree
+ t.index ["sha"], name: "index_merge_request_diff_commits_on_sha", using: :btree
end
- add_index "merge_request_diff_commits", ["merge_request_diff_id", "relative_order"], name: "index_merge_request_diff_commits_on_mr_diff_id_and_order", unique: true, using: :btree
- add_index "merge_request_diff_commits", ["sha"], name: "index_merge_request_diff_commits_on_sha", using: :btree
-
create_table "merge_request_diff_files", id: false, force: :cascade do |t|
t.integer "merge_request_diff_id", null: false
t.integer "relative_order", null: false
@@ -1244,10 +1165,9 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.text "old_path", null: false
t.text "diff", null: false
t.boolean "binary"
+ 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
- add_index "merge_request_diff_files", ["merge_request_diff_id", "relative_order"], name: "index_merge_request_diff_files_on_mr_diff_id_and_order", unique: true, using: :btree
-
create_table "merge_request_diffs", force: :cascade do |t|
t.string "state"
t.integer "merge_request_id", null: false
@@ -1258,10 +1178,9 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.string "head_commit_sha"
t.string "start_commit_sha"
t.integer "commits_count"
+ t.index ["merge_request_id", "id"], name: "index_merge_request_diffs_on_merge_request_id_and_id", using: :btree
end
- add_index "merge_request_diffs", ["merge_request_id", "id"], name: "index_merge_request_diffs_on_merge_request_id_and_id", using: :btree
-
create_table "merge_request_metrics", force: :cascade do |t|
t.integer "merge_request_id", null: false
t.datetime "latest_build_started_at"
@@ -1274,12 +1193,11 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.integer "merged_by_id"
t.integer "latest_closed_by_id"
t.datetime_with_timezone "latest_closed_at"
+ t.index ["first_deployed_to_production_at"], name: "index_merge_request_metrics_on_first_deployed_to_production_at", using: :btree
+ t.index ["merge_request_id"], name: "index_merge_request_metrics", using: :btree
+ t.index ["pipeline_id"], name: "index_merge_request_metrics_on_pipeline_id", using: :btree
end
- add_index "merge_request_metrics", ["first_deployed_to_production_at"], name: "index_merge_request_metrics_on_first_deployed_to_production_at", using: :btree
- add_index "merge_request_metrics", ["merge_request_id"], name: "index_merge_request_metrics", using: :btree
- add_index "merge_request_metrics", ["pipeline_id"], name: "index_merge_request_metrics_on_pipeline_id", using: :btree
-
create_table "merge_requests", force: :cascade do |t|
t.string "target_branch", null: false
t.string "source_branch", null: false
@@ -1316,37 +1234,36 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.string "rebase_commit_sha"
t.boolean "squash", default: false, null: false
t.boolean "allow_maintainer_to_push"
+ t.index ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
+ t.index ["author_id"], name: "index_merge_requests_on_author_id", using: :btree
+ t.index ["created_at"], name: "index_merge_requests_on_created_at", using: :btree
+ t.index ["description"], name: "index_merge_requests_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
+ t.index ["head_pipeline_id"], name: "index_merge_requests_on_head_pipeline_id", using: :btree
+ t.index ["id", "merge_jid"], name: "index_merge_requests_on_id_and_merge_jid", where: "((merge_jid IS NOT NULL) AND ((state)::text = 'locked'::text))", using: :btree
+ t.index ["latest_merge_request_diff_id"], name: "index_merge_requests_on_latest_merge_request_diff_id", using: :btree
+ t.index ["merge_user_id"], name: "index_merge_requests_on_merge_user_id", where: "(merge_user_id IS NOT NULL)", using: :btree
+ t.index ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree
+ t.index ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree
+ t.index ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_and_branch_state_opened", where: "((state)::text = 'opened'::text)", using: :btree
+ t.index ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_id_and_source_branch", using: :btree
+ t.index ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree
+ t.index ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true, using: :btree
+ t.index ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid_opened", where: "((state)::text = 'opened'::text)", using: :btree
+ t.index ["target_project_id", "merge_commit_sha", "id"], name: "index_merge_requests_on_tp_id_and_merge_commit_sha_and_id", using: :btree
+ t.index ["title"], name: "index_merge_requests_on_title", using: :btree
+ t.index ["title"], name: "index_merge_requests_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
+ t.index ["updated_by_id"], name: "index_merge_requests_on_updated_by_id", where: "(updated_by_id IS NOT NULL)", using: :btree
end
- add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
- add_index "merge_requests", ["author_id"], name: "index_merge_requests_on_author_id", using: :btree
- add_index "merge_requests", ["created_at"], name: "index_merge_requests_on_created_at", using: :btree
- add_index "merge_requests", ["description"], name: "index_merge_requests_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
- add_index "merge_requests", ["head_pipeline_id"], name: "index_merge_requests_on_head_pipeline_id", using: :btree
- add_index "merge_requests", ["latest_merge_request_diff_id"], name: "index_merge_requests_on_latest_merge_request_diff_id", using: :btree
- add_index "merge_requests", ["merge_user_id"], name: "index_merge_requests_on_merge_user_id", where: "(merge_user_id IS NOT NULL)", using: :btree
- add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree
- add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree
- add_index "merge_requests", ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_and_branch_state_opened", where: "((state)::text = 'opened'::text)", using: :btree
- add_index "merge_requests", ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_id_and_source_branch", using: :btree
- add_index "merge_requests", ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree
- add_index "merge_requests", ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true, using: :btree
- add_index "merge_requests", ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid_opened", where: "((state)::text = 'opened'::text)", using: :btree
- add_index "merge_requests", ["target_project_id", "merge_commit_sha", "id"], name: "index_merge_requests_on_tp_id_and_merge_commit_sha_and_id", using: :btree
- add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree
- add_index "merge_requests", ["title"], name: "index_merge_requests_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
- add_index "merge_requests", ["updated_by_id"], name: "index_merge_requests_on_updated_by_id", where: "(updated_by_id IS NOT NULL)", using: :btree
-
create_table "merge_requests_closing_issues", force: :cascade do |t|
t.integer "merge_request_id", null: false
t.integer "issue_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.index ["issue_id"], name: "index_merge_requests_closing_issues_on_issue_id", using: :btree
+ t.index ["merge_request_id"], name: "index_merge_requests_closing_issues_on_merge_request_id", using: :btree
end
- add_index "merge_requests_closing_issues", ["issue_id"], name: "index_merge_requests_closing_issues_on_issue_id", using: :btree
- add_index "merge_requests_closing_issues", ["merge_request_id"], name: "index_merge_requests_closing_issues_on_merge_request_id", using: :btree
-
create_table "milestones", force: :cascade do |t|
t.string "title", null: false
t.integer "project_id"
@@ -1361,15 +1278,14 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.date "start_date"
t.integer "cached_markdown_version"
t.integer "group_id"
+ t.index ["description"], name: "index_milestones_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
+ t.index ["due_date"], name: "index_milestones_on_due_date", using: :btree
+ t.index ["group_id"], name: "index_milestones_on_group_id", using: :btree
+ t.index ["project_id", "iid"], name: "index_milestones_on_project_id_and_iid", unique: true, using: :btree
+ t.index ["title"], name: "index_milestones_on_title", using: :btree
+ t.index ["title"], name: "index_milestones_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
end
- add_index "milestones", ["description"], name: "index_milestones_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
- add_index "milestones", ["due_date"], name: "index_milestones_on_due_date", using: :btree
- add_index "milestones", ["group_id"], name: "index_milestones_on_group_id", using: :btree
- add_index "milestones", ["project_id", "iid"], name: "index_milestones_on_project_id_and_iid", unique: true, using: :btree
- add_index "milestones", ["title"], name: "index_milestones_on_title", using: :btree
- add_index "milestones", ["title"], name: "index_milestones_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
-
create_table "namespaces", force: :cascade do |t|
t.string "name", null: false
t.string "path", null: false
@@ -1389,19 +1305,18 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.integer "two_factor_grace_period", default: 48, null: false
t.integer "cached_markdown_version"
t.string "runners_token"
+ t.index ["created_at"], name: "index_namespaces_on_created_at", using: :btree
+ t.index ["name", "parent_id"], name: "index_namespaces_on_name_and_parent_id", unique: true, using: :btree
+ t.index ["name"], name: "index_namespaces_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
+ t.index ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree
+ t.index ["parent_id", "id"], name: "index_namespaces_on_parent_id_and_id", unique: true, using: :btree
+ t.index ["path"], name: "index_namespaces_on_path", using: :btree
+ t.index ["path"], name: "index_namespaces_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"}
+ t.index ["require_two_factor_authentication"], name: "index_namespaces_on_require_two_factor_authentication", using: :btree
+ t.index ["runners_token"], name: "index_namespaces_on_runners_token", unique: true, using: :btree
+ t.index ["type"], name: "index_namespaces_on_type", using: :btree
end
- add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree
- add_index "namespaces", ["name", "parent_id"], name: "index_namespaces_on_name_and_parent_id", unique: true, using: :btree
- add_index "namespaces", ["name"], name: "index_namespaces_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
- add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree
- add_index "namespaces", ["parent_id", "id"], name: "index_namespaces_on_parent_id_and_id", unique: true, using: :btree
- add_index "namespaces", ["path"], name: "index_namespaces_on_path", using: :btree
- add_index "namespaces", ["path"], name: "index_namespaces_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"}
- add_index "namespaces", ["require_two_factor_authentication"], name: "index_namespaces_on_require_two_factor_authentication", using: :btree
- add_index "namespaces", ["runners_token"], name: "index_namespaces_on_runners_token", unique: true, using: :btree
- add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
-
create_table "note_diff_files", force: :cascade do |t|
t.integer "diff_note_id", null: false
t.text "diff", null: false
@@ -1412,10 +1327,9 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.string "b_mode", null: false
t.text "new_path", null: false
t.text "old_path", null: false
+ t.index ["diff_note_id"], name: "index_note_diff_files_on_diff_note_id", unique: true, using: :btree
end
- add_index "note_diff_files", ["diff_note_id"], name: "index_note_diff_files_on_diff_note_id", unique: true, using: :btree
-
create_table "notes", force: :cascade do |t|
t.text "note"
t.string "noteable_type"
@@ -1440,19 +1354,18 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.integer "cached_markdown_version"
t.text "change_position"
t.boolean "resolved_by_push"
+ t.index ["author_id"], name: "index_notes_on_author_id", using: :btree
+ t.index ["commit_id"], name: "index_notes_on_commit_id", using: :btree
+ t.index ["created_at"], name: "index_notes_on_created_at", using: :btree
+ t.index ["discussion_id"], name: "index_notes_on_discussion_id", using: :btree
+ t.index ["line_code"], name: "index_notes_on_line_code", using: :btree
+ t.index ["note"], name: "index_notes_on_note_trigram", using: :gin, opclasses: {"note"=>"gin_trgm_ops"}
+ t.index ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree
+ t.index ["noteable_type"], name: "index_notes_on_noteable_type", using: :btree
+ t.index ["project_id", "noteable_type"], name: "index_notes_on_project_id_and_noteable_type", using: :btree
+ t.index ["updated_at"], name: "index_notes_on_updated_at", using: :btree
end
- add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree
- add_index "notes", ["commit_id"], name: "index_notes_on_commit_id", using: :btree
- add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree
- add_index "notes", ["discussion_id"], name: "index_notes_on_discussion_id", using: :btree
- add_index "notes", ["line_code"], name: "index_notes_on_line_code", using: :btree
- add_index "notes", ["note"], name: "index_notes_on_note_trigram", using: :gin, opclasses: {"note"=>"gin_trgm_ops"}
- add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree
- add_index "notes", ["noteable_type"], name: "index_notes_on_noteable_type", using: :btree
- add_index "notes", ["project_id", "noteable_type"], name: "index_notes_on_project_id_and_noteable_type", using: :btree
- add_index "notes", ["updated_at"], name: "index_notes_on_updated_at", using: :btree
-
create_table "notification_settings", force: :cascade do |t|
t.integer "user_id", null: false
t.integer "source_id"
@@ -1474,12 +1387,11 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.boolean "success_pipeline"
t.boolean "push_to_merge_request"
t.boolean "issue_due"
+ t.index ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type", using: :btree
+ t.index ["user_id", "source_id", "source_type"], name: "index_notifications_on_user_id_and_source_id_and_source_type", unique: true, using: :btree
+ t.index ["user_id"], name: "index_notification_settings_on_user_id", using: :btree
end
- add_index "notification_settings", ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type", using: :btree
- add_index "notification_settings", ["user_id", "source_id", "source_type"], name: "index_notifications_on_user_id_and_source_id_and_source_type", unique: true, using: :btree
- add_index "notification_settings", ["user_id"], name: "index_notification_settings_on_user_id", using: :btree
-
create_table "oauth_access_grants", force: :cascade do |t|
t.integer "resource_owner_id", null: false
t.integer "application_id", null: false
@@ -1489,10 +1401,9 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.datetime "created_at", null: false
t.datetime "revoked_at"
t.string "scopes"
+ t.index ["token"], name: "index_oauth_access_grants_on_token", unique: true, using: :btree
end
- add_index "oauth_access_grants", ["token"], name: "index_oauth_access_grants_on_token", unique: true, using: :btree
-
create_table "oauth_access_tokens", force: :cascade do |t|
t.integer "resource_owner_id"
t.integer "application_id"
@@ -1502,12 +1413,11 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.datetime "revoked_at"
t.datetime "created_at", null: false
t.string "scopes"
+ t.index ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true, using: :btree
+ t.index ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id", using: :btree
+ t.index ["token"], name: "index_oauth_access_tokens_on_token", unique: true, using: :btree
end
- add_index "oauth_access_tokens", ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true, using: :btree
- add_index "oauth_access_tokens", ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id", using: :btree
- add_index "oauth_access_tokens", ["token"], name: "index_oauth_access_tokens_on_token", unique: true, using: :btree
-
create_table "oauth_applications", force: :cascade do |t|
t.string "name", null: false
t.string "uid", null: false
@@ -1519,11 +1429,10 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.integer "owner_id"
t.string "owner_type"
t.boolean "trusted", default: false, null: false
+ t.index ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree
+ t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree
end
- add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree
- add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree
-
create_table "oauth_openid_requests", force: :cascade do |t|
t.integer "access_grant_id", null: false
t.string "nonce", null: false
@@ -1539,14 +1448,13 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.datetime_with_timezone "verified_at"
t.string "verification_code", null: false
t.datetime_with_timezone "enabled_until"
+ 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
+ t.index ["verified_at", "enabled_until"], name: "index_pages_domains_on_verified_at_and_enabled_until", using: :btree
+ t.index ["verified_at"], name: "index_pages_domains_on_verified_at", using: :btree
end
- add_index "pages_domains", ["domain"], name: "index_pages_domains_on_domain", unique: true, using: :btree
- add_index "pages_domains", ["project_id", "enabled_until"], name: "index_pages_domains_on_project_id_and_enabled_until", using: :btree
- add_index "pages_domains", ["project_id"], name: "index_pages_domains_on_project_id", using: :btree
- add_index "pages_domains", ["verified_at", "enabled_until"], name: "index_pages_domains_on_verified_at_and_enabled_until", using: :btree
- add_index "pages_domains", ["verified_at"], name: "index_pages_domains_on_verified_at", using: :btree
-
create_table "personal_access_tokens", force: :cascade do |t|
t.integer "user_id", null: false
t.string "token"
@@ -1558,29 +1466,26 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.string "scopes", default: "--- []\n", null: false
t.boolean "impersonation", default: false, null: false
t.string "token_digest"
+ t.index ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree
+ t.index ["token_digest"], name: "index_personal_access_tokens_on_token_digest", unique: true, using: :btree
+ t.index ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree
end
- add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree
- add_index "personal_access_tokens", ["token_digest"], name: "index_personal_access_tokens_on_token_digest", unique: true, using: :btree
- add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree
-
create_table "programming_languages", force: :cascade do |t|
t.string "name", null: false
t.string "color", null: false
t.datetime_with_timezone "created_at", null: false
+ t.index ["name"], name: "index_programming_languages_on_name", unique: true, using: :btree
end
- add_index "programming_languages", ["name"], name: "index_programming_languages_on_name", unique: true, using: :btree
-
create_table "project_authorizations", id: false, force: :cascade do |t|
t.integer "user_id", null: false
t.integer "project_id", null: false
t.integer "access_level", null: false
+ t.index ["project_id"], name: "index_project_authorizations_on_project_id", using: :btree
+ t.index ["user_id", "project_id", "access_level"], name: "index_project_authorizations_on_user_id_project_id_access_level", unique: true, using: :btree
end
- add_index "project_authorizations", ["project_id"], name: "index_project_authorizations_on_project_id", using: :btree
- add_index "project_authorizations", ["user_id", "project_id", "access_level"], name: "index_project_authorizations_on_user_id_project_id_access_level", unique: true, using: :btree
-
create_table "project_auto_devops", force: :cascade do |t|
t.integer "project_id", null: false
t.datetime_with_timezone "created_at", null: false
@@ -1588,51 +1493,47 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.boolean "enabled"
t.string "domain"
t.integer "deploy_strategy", default: 0, null: false
+ t.index ["project_id"], name: "index_project_auto_devops_on_project_id", unique: true, using: :btree
end
- add_index "project_auto_devops", ["project_id"], name: "index_project_auto_devops_on_project_id", unique: true, using: :btree
-
create_table "project_ci_cd_settings", force: :cascade do |t|
t.integer "project_id", null: false
t.boolean "group_runners_enabled", default: true, null: false
+ t.index ["project_id"], name: "index_project_ci_cd_settings_on_project_id", unique: true, using: :btree
end
- add_index "project_ci_cd_settings", ["project_id"], name: "index_project_ci_cd_settings_on_project_id", unique: true, using: :btree
-
create_table "project_custom_attributes", 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
t.string "key", null: false
t.string "value", null: false
+ t.index ["key", "value"], name: "index_project_custom_attributes_on_key_and_value", using: :btree
+ t.index ["project_id", "key"], name: "index_project_custom_attributes_on_project_id_and_key", unique: true, using: :btree
end
- add_index "project_custom_attributes", ["key", "value"], name: "index_project_custom_attributes_on_key_and_value", using: :btree
- add_index "project_custom_attributes", ["project_id", "key"], name: "index_project_custom_attributes_on_project_id_and_key", unique: true, using: :btree
-
create_table "project_deploy_tokens", 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
+ t.index ["deploy_token_id"], name: "index_project_deploy_tokens_on_deploy_token_id", using: :btree
+ t.index ["project_id", "deploy_token_id"], name: "index_project_deploy_tokens_on_project_id_and_deploy_token_id", unique: true, using: :btree
end
- add_index "project_deploy_tokens", ["project_id", "deploy_token_id"], name: "index_project_deploy_tokens_on_project_id_and_deploy_token_id", unique: true, using: :btree
-
create_table "project_features", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "merge_requests_access_level"
t.integer "issues_access_level"
t.integer "wiki_access_level"
- t.integer "snippets_access_level"
+ t.integer "snippets_access_level", default: 20, null: false
t.integer "builds_access_level"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "repository_access_level", default: 20, null: false
t.integer "pages_access_level", default: 20, null: false
+ t.index ["project_id"], name: "index_project_features_on_project_id", unique: true, using: :btree
end
- add_index "project_features", ["project_id"], name: "index_project_features_on_project_id", unique: true, using: :btree
-
create_table "project_group_links", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "group_id", null: false
@@ -1640,45 +1541,41 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.datetime "updated_at"
t.integer "group_access", default: 30, null: false
t.date "expires_at"
+ t.index ["group_id"], name: "index_project_group_links_on_group_id", using: :btree
+ t.index ["project_id"], name: "index_project_group_links_on_project_id", using: :btree
end
- add_index "project_group_links", ["group_id"], name: "index_project_group_links_on_group_id", using: :btree
- add_index "project_group_links", ["project_id"], name: "index_project_group_links_on_project_id", using: :btree
-
create_table "project_import_data", force: :cascade do |t|
t.integer "project_id"
t.text "data"
t.text "encrypted_credentials"
t.string "encrypted_credentials_iv"
t.string "encrypted_credentials_salt"
+ t.index ["project_id"], name: "index_project_import_data_on_project_id", using: :btree
end
- add_index "project_import_data", ["project_id"], name: "index_project_import_data_on_project_id", using: :btree
-
create_table "project_mirror_data", force: :cascade do |t|
t.integer "project_id", null: false
t.string "status"
t.string "jid"
t.text "last_error"
+ t.index ["jid"], name: "index_project_mirror_data_on_jid", using: :btree
+ t.index ["project_id"], name: "index_project_mirror_data_on_project_id", unique: true, using: :btree
+ t.index ["status"], name: "index_project_mirror_data_on_status", using: :btree
end
- add_index "project_mirror_data", ["jid"], name: "index_project_mirror_data_on_jid", using: :btree
- add_index "project_mirror_data", ["project_id"], name: "index_project_mirror_data_on_project_id", unique: true, using: :btree
- add_index "project_mirror_data", ["status"], name: "index_project_mirror_data_on_status", using: :btree
-
create_table "project_statistics", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "namespace_id", null: false
- t.integer "commit_count", limit: 8, default: 0, null: false
- t.integer "storage_size", limit: 8, default: 0, null: false
- t.integer "repository_size", limit: 8, default: 0, null: false
- t.integer "lfs_objects_size", limit: 8, default: 0, null: false
- t.integer "build_artifacts_size", limit: 8, default: 0, null: false
+ t.bigint "commit_count", default: 0, null: false
+ t.bigint "storage_size", default: 0, null: false
+ 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.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
- add_index "project_statistics", ["namespace_id"], name: "index_project_statistics_on_namespace_id", using: :btree
- add_index "project_statistics", ["project_id"], name: "index_project_statistics_on_project_id", unique: true, using: :btree
-
create_table "projects", force: :cascade do |t|
t.string "name"
t.string "path"
@@ -1731,28 +1628,29 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.integer "jobs_cache_index"
t.boolean "pages_https_only", default: true
t.boolean "remote_mirror_available_overridden"
+ t.bigint "pool_repository_id"
+ 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"}
+ t.index ["id"], name: "index_projects_on_id_partial_for_visibility", unique: true, where: "(visibility_level = ANY (ARRAY[10, 20]))", using: :btree
+ t.index ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree
+ t.index ["last_repository_check_at"], name: "index_projects_on_last_repository_check_at", where: "(last_repository_check_at IS NOT NULL)", using: :btree
+ t.index ["last_repository_check_failed"], name: "index_projects_on_last_repository_check_failed", using: :btree
+ t.index ["last_repository_updated_at"], name: "index_projects_on_last_repository_updated_at", using: :btree
+ t.index ["name"], name: "index_projects_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
+ t.index ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree
+ t.index ["path"], name: "index_projects_on_path", using: :btree
+ t.index ["path"], name: "index_projects_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"}
+ t.index ["pending_delete"], name: "index_projects_on_pending_delete", using: :btree
+ t.index ["pool_repository_id"], name: "index_projects_on_pool_repository_id", where: "(pool_repository_id IS NOT NULL)", using: :btree
+ t.index ["repository_storage", "created_at"], name: "idx_project_repository_check_partial", where: "(last_repository_check_at IS NULL)", using: :btree
+ t.index ["repository_storage"], name: "index_projects_on_repository_storage", using: :btree
+ t.index ["runners_token"], name: "index_projects_on_runners_token", using: :btree
+ t.index ["star_count"], name: "index_projects_on_star_count", using: :btree
+ t.index ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree
end
- add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
- add_index "projects", ["created_at"], name: "index_projects_on_created_at", using: :btree
- add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree
- add_index "projects", ["description"], name: "index_projects_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
- add_index "projects", ["id"], name: "index_projects_on_id_partial_for_visibility", unique: true, where: "(visibility_level = ANY (ARRAY[10, 20]))", using: :btree
- add_index "projects", ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree
- add_index "projects", ["last_repository_check_at"], name: "index_projects_on_last_repository_check_at", where: "(last_repository_check_at IS NOT NULL)", using: :btree
- add_index "projects", ["last_repository_check_failed"], name: "index_projects_on_last_repository_check_failed", using: :btree
- add_index "projects", ["last_repository_updated_at"], name: "index_projects_on_last_repository_updated_at", using: :btree
- add_index "projects", ["name"], name: "index_projects_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
- add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree
- add_index "projects", ["path"], name: "index_projects_on_path", using: :btree
- add_index "projects", ["path"], name: "index_projects_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"}
- add_index "projects", ["pending_delete"], name: "index_projects_on_pending_delete", using: :btree
- add_index "projects", ["repository_storage", "created_at"], name: "idx_project_repository_check_partial", where: "(last_repository_check_at IS NULL)", using: :btree
- add_index "projects", ["repository_storage"], name: "index_projects_on_repository_storage", using: :btree
- add_index "projects", ["runners_token"], name: "index_projects_on_runners_token", using: :btree
- add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree
- add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree
-
create_table "prometheus_metrics", force: :cascade do |t|
t.integer "project_id"
t.string "title", null: false
@@ -1765,40 +1663,36 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.datetime_with_timezone "updated_at", null: false
t.boolean "common", default: false, null: false
t.string "identifier"
+ t.index ["common"], name: "index_prometheus_metrics_on_common", using: :btree
+ t.index ["group"], name: "index_prometheus_metrics_on_group", using: :btree
+ t.index ["identifier"], name: "index_prometheus_metrics_on_identifier", unique: true, using: :btree
+ t.index ["project_id"], name: "index_prometheus_metrics_on_project_id", using: :btree
end
- add_index "prometheus_metrics", ["common"], name: "index_prometheus_metrics_on_common", using: :btree
- add_index "prometheus_metrics", ["group"], name: "index_prometheus_metrics_on_group", using: :btree
- add_index "prometheus_metrics", ["identifier"], name: "index_prometheus_metrics_on_identifier", unique: true, using: :btree
- add_index "prometheus_metrics", ["project_id"], name: "index_prometheus_metrics_on_project_id", using: :btree
-
create_table "protected_branch_merge_access_levels", 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
t.datetime "updated_at", null: false
+ t.index ["protected_branch_id"], name: "index_protected_branch_merge_access", using: :btree
end
- add_index "protected_branch_merge_access_levels", ["protected_branch_id"], name: "index_protected_branch_merge_access", using: :btree
-
create_table "protected_branch_push_access_levels", 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
t.datetime "updated_at", null: false
+ t.index ["protected_branch_id"], name: "index_protected_branch_push_access", using: :btree
end
- add_index "protected_branch_push_access_levels", ["protected_branch_id"], name: "index_protected_branch_push_access", using: :btree
-
create_table "protected_branches", force: :cascade do |t|
t.integer "project_id", null: false
t.string "name", null: false
t.datetime "created_at"
t.datetime "updated_at"
+ t.index ["project_id"], name: "index_protected_branches_on_project_id", using: :btree
end
- add_index "protected_branches", ["project_id"], name: "index_protected_branches_on_project_id", using: :btree
-
create_table "protected_tag_create_access_levels", force: :cascade do |t|
t.integer "protected_tag_id", null: false
t.integer "access_level", default: 40
@@ -1806,23 +1700,21 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.integer "group_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.index ["protected_tag_id"], name: "index_protected_tag_create_access", using: :btree
+ t.index ["user_id"], name: "index_protected_tag_create_access_levels_on_user_id", using: :btree
end
- add_index "protected_tag_create_access_levels", ["protected_tag_id"], name: "index_protected_tag_create_access", using: :btree
- add_index "protected_tag_create_access_levels", ["user_id"], name: "index_protected_tag_create_access_levels_on_user_id", using: :btree
-
create_table "protected_tags", force: :cascade do |t|
t.integer "project_id", null: false
t.string "name", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.index ["project_id", "name"], name: "index_protected_tags_on_project_id_and_name", unique: true, using: :btree
+ t.index ["project_id"], name: "index_protected_tags_on_project_id", using: :btree
end
- add_index "protected_tags", ["project_id", "name"], name: "index_protected_tags_on_project_id_and_name", unique: true, using: :btree
- add_index "protected_tags", ["project_id"], name: "index_protected_tags_on_project_id", using: :btree
-
create_table "push_event_payloads", id: false, force: :cascade do |t|
- t.integer "commit_count", limit: 8, null: false
+ t.bigint "commit_count", null: false
t.integer "event_id", null: false
t.integer "action", limit: 2, null: false
t.integer "ref_type", limit: 2, null: false
@@ -1830,21 +1722,19 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.binary "commit_to"
t.text "ref"
t.string "commit_title", limit: 70
+ t.index ["event_id"], name: "index_push_event_payloads_on_event_id", unique: true, using: :btree
end
- add_index "push_event_payloads", ["event_id"], name: "index_push_event_payloads_on_event_id", unique: true, using: :btree
-
create_table "redirect_routes", force: :cascade do |t|
t.integer "source_id", null: false
t.string "source_type", null: false
t.string "path", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.index ["path"], name: "index_redirect_routes_on_path", unique: true, using: :btree
+ t.index ["source_type", "source_id"], name: "index_redirect_routes_on_source_type_and_source_id", using: :btree
end
- add_index "redirect_routes", ["path"], name: "index_redirect_routes_on_path", unique: true, using: :btree
- add_index "redirect_routes", ["source_type", "source_id"], name: "index_redirect_routes_on_source_type_and_source_id", using: :btree
-
create_table "releases", force: :cascade do |t|
t.string "tag"
t.text "description"
@@ -1853,11 +1743,10 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.datetime "updated_at"
t.text "description_html"
t.integer "cached_markdown_version"
+ t.index ["project_id", "tag"], name: "index_releases_on_project_id_and_tag", using: :btree
+ t.index ["project_id"], name: "index_releases_on_project_id", using: :btree
end
- add_index "releases", ["project_id", "tag"], name: "index_releases_on_project_id_and_tag", using: :btree
- add_index "releases", ["project_id"], name: "index_releases_on_project_id", using: :btree
-
create_table "remote_mirrors", force: :cascade do |t|
t.integer "project_id"
t.string "url"
@@ -1874,19 +1763,24 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.string "encrypted_credentials_salt"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.index ["last_successful_update_at"], name: "index_remote_mirrors_on_last_successful_update_at", using: :btree
+ t.index ["project_id"], name: "index_remote_mirrors_on_project_id", using: :btree
end
- add_index "remote_mirrors", ["last_successful_update_at"], name: "index_remote_mirrors_on_last_successful_update_at", using: :btree
- add_index "remote_mirrors", ["project_id"], name: "index_remote_mirrors_on_project_id", using: :btree
+ create_table "repositories", id: :bigserial, force: :cascade do |t|
+ t.integer "shard_id", null: false
+ t.string "disk_path", null: false
+ t.index ["disk_path"], name: "index_repositories_on_disk_path", unique: true, using: :btree
+ t.index ["shard_id"], name: "index_repositories_on_shard_id", using: :btree
+ end
create_table "repository_languages", id: false, force: :cascade do |t|
t.integer "project_id", null: false
t.integer "programming_language_id", null: false
t.float "share", null: false
+ t.index ["project_id", "programming_language_id"], name: "index_repository_languages_on_project_and_languages_id", unique: true, using: :btree
end
- add_index "repository_languages", ["project_id", "programming_language_id"], name: "index_repository_languages_on_project_and_languages_id", unique: true, using: :btree
-
create_table "resource_label_events", id: :bigserial, force: :cascade do |t|
t.integer "action", null: false
t.integer "issue_id"
@@ -1897,13 +1791,12 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.integer "cached_markdown_version"
t.text "reference"
t.text "reference_html"
+ t.index ["issue_id"], name: "index_resource_label_events_on_issue_id", using: :btree
+ t.index ["label_id"], name: "index_resource_label_events_on_label_id", using: :btree
+ t.index ["merge_request_id"], name: "index_resource_label_events_on_merge_request_id", using: :btree
+ t.index ["user_id"], name: "index_resource_label_events_on_user_id", using: :btree
end
- add_index "resource_label_events", ["issue_id"], name: "index_resource_label_events_on_issue_id", using: :btree
- add_index "resource_label_events", ["label_id"], name: "index_resource_label_events_on_label_id", using: :btree
- add_index "resource_label_events", ["merge_request_id"], name: "index_resource_label_events_on_merge_request_id", using: :btree
- add_index "resource_label_events", ["user_id"], name: "index_resource_label_events_on_user_id", using: :btree
-
create_table "routes", force: :cascade do |t|
t.integer "source_id", null: false
t.string "source_type", null: false
@@ -1911,12 +1804,11 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.datetime "created_at"
t.datetime "updated_at"
t.string "name"
+ t.index ["path"], name: "index_routes_on_path", unique: true, using: :btree
+ t.index ["path"], name: "index_routes_on_path_text_pattern_ops", using: :btree, opclasses: {"path"=>"varchar_pattern_ops"}
+ t.index ["source_type", "source_id"], name: "index_routes_on_source_type_and_source_id", unique: true, using: :btree
end
- add_index "routes", ["path"], name: "index_routes_on_path", unique: true, using: :btree
- add_index "routes", ["path"], name: "index_routes_on_path_text_pattern_ops", using: :btree, opclasses: {"path"=>"varchar_pattern_ops"}
- add_index "routes", ["source_type", "source_id"], name: "index_routes_on_source_type_and_source_id", unique: true, using: :btree
-
create_table "sent_notifications", force: :cascade do |t|
t.integer "project_id"
t.integer "noteable_id"
@@ -1928,10 +1820,9 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.string "note_type"
t.text "position"
t.string "in_reply_to_discussion_id"
+ t.index ["reply_key"], name: "index_sent_notifications_on_reply_key", unique: true, using: :btree
end
- add_index "sent_notifications", ["reply_key"], name: "index_sent_notifications_on_reply_key", unique: true, using: :btree
-
create_table "services", force: :cascade do |t|
t.string "type"
t.string "title"
@@ -1954,10 +1845,14 @@ ActiveRecord::Schema.define(version: 20181017001059) 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.index ["project_id"], name: "index_services_on_project_id", using: :btree
+ t.index ["template"], name: "index_services_on_template", using: :btree
end
- add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
- add_index "services", ["template"], name: "index_services_on_template", using: :btree
+ create_table "shards", force: :cascade do |t|
+ t.string "name", null: false
+ t.index ["name"], name: "index_shards_on_name", unique: true, using: :btree
+ end
create_table "site_statistics", force: :cascade do |t|
t.integer "repositories_count", default: 0, null: false
@@ -1978,15 +1873,14 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.integer "cached_markdown_version"
t.text "description"
t.text "description_html"
+ t.index ["author_id"], name: "index_snippets_on_author_id", using: :btree
+ t.index ["file_name"], name: "index_snippets_on_file_name_trigram", using: :gin, opclasses: {"file_name"=>"gin_trgm_ops"}
+ t.index ["project_id"], name: "index_snippets_on_project_id", using: :btree
+ t.index ["title"], name: "index_snippets_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
+ t.index ["updated_at"], name: "index_snippets_on_updated_at", using: :btree
+ t.index ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree
end
- add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree
- add_index "snippets", ["file_name"], name: "index_snippets_on_file_name_trigram", using: :gin, opclasses: {"file_name"=>"gin_trgm_ops"}
- add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree
- add_index "snippets", ["title"], name: "index_snippets_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
- add_index "snippets", ["updated_at"], name: "index_snippets_on_updated_at", using: :btree
- add_index "snippets", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree
-
create_table "spam_logs", force: :cascade do |t|
t.integer "user_id"
t.string "source_ip"
@@ -2009,20 +1903,18 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.datetime "created_at"
t.datetime "updated_at"
t.integer "project_id"
+ 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
- add_index "subscriptions", ["subscribable_id", "subscribable_type", "user_id", "project_id"], name: "index_subscriptions_on_subscribable_and_user_id_and_project_id", unique: true, using: :btree
-
create_table "system_note_metadata", force: :cascade do |t|
t.integer "note_id", null: false
t.integer "commit_count"
t.string "action"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.index ["note_id"], name: "index_system_note_metadata_on_note_id", unique: true, using: :btree
end
- add_index "system_note_metadata", ["note_id"], name: "index_system_note_metadata_on_note_id", unique: true, using: :btree
-
create_table "taggings", force: :cascade do |t|
t.integer "tag_id"
t.integer "taggable_id"
@@ -2031,32 +1923,29 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.string "tagger_type"
t.string "context"
t.datetime "created_at"
+ t.index ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "taggings_idx", unique: true, using: :btree
+ t.index ["tag_id"], name: "index_taggings_on_tag_id", using: :btree
+ t.index ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
+ t.index ["taggable_id", "taggable_type"], name: "index_taggings_on_taggable_id_and_taggable_type", using: :btree
end
- add_index "taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "taggings_idx", unique: true, using: :btree
- add_index "taggings", ["tag_id"], name: "index_taggings_on_tag_id", using: :btree
- add_index "taggings", ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
- add_index "taggings", ["taggable_id", "taggable_type"], name: "index_taggings_on_taggable_id_and_taggable_type", using: :btree
-
create_table "tags", 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
end
- add_index "tags", ["name"], name: "index_tags_on_name", unique: true, using: :btree
-
create_table "term_agreements", force: :cascade do |t|
t.integer "term_id", null: false
t.integer "user_id", null: false
t.boolean "accepted", default: false, null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
+ t.index ["term_id"], name: "index_term_agreements_on_term_id", using: :btree
+ t.index ["user_id", "term_id"], name: "term_agreements_unique_index", unique: true, using: :btree
+ t.index ["user_id"], name: "index_term_agreements_on_user_id", using: :btree
end
- add_index "term_agreements", ["term_id"], name: "index_term_agreements_on_term_id", using: :btree
- add_index "term_agreements", ["user_id", "term_id"], name: "term_agreements_unique_index", unique: true, using: :btree
- add_index "term_agreements", ["user_id"], name: "index_term_agreements_on_user_id", using: :btree
-
create_table "timelogs", force: :cascade do |t|
t.integer "time_spent", null: false
t.integer "user_id"
@@ -2065,12 +1954,11 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.integer "issue_id"
t.integer "merge_request_id"
t.datetime_with_timezone "spent_at"
+ t.index ["issue_id"], name: "index_timelogs_on_issue_id", using: :btree
+ t.index ["merge_request_id"], name: "index_timelogs_on_merge_request_id", using: :btree
+ t.index ["user_id"], name: "index_timelogs_on_user_id", using: :btree
end
- add_index "timelogs", ["issue_id"], name: "index_timelogs_on_issue_id", using: :btree
- add_index "timelogs", ["merge_request_id"], name: "index_timelogs_on_merge_request_id", using: :btree
- add_index "timelogs", ["user_id"], name: "index_timelogs_on_user_id", using: :btree
-
create_table "todos", force: :cascade do |t|
t.integer "user_id", null: false
t.integer "project_id"
@@ -2084,24 +1972,22 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.integer "note_id"
t.string "commit_id"
t.integer "group_id"
+ t.index ["author_id"], name: "index_todos_on_author_id", using: :btree
+ t.index ["commit_id"], name: "index_todos_on_commit_id", using: :btree
+ t.index ["group_id"], name: "index_todos_on_group_id", using: :btree
+ t.index ["note_id"], name: "index_todos_on_note_id", using: :btree
+ t.index ["project_id"], name: "index_todos_on_project_id", using: :btree
+ t.index ["target_type", "target_id"], name: "index_todos_on_target_type_and_target_id", using: :btree
+ t.index ["user_id", "id"], name: "index_todos_on_user_id_and_id_done", where: "((state)::text = 'done'::text)", using: :btree
+ t.index ["user_id", "id"], name: "index_todos_on_user_id_and_id_pending", where: "((state)::text = 'pending'::text)", using: :btree
+ t.index ["user_id"], name: "index_todos_on_user_id", using: :btree
end
- add_index "todos", ["author_id"], name: "index_todos_on_author_id", using: :btree
- add_index "todos", ["commit_id"], name: "index_todos_on_commit_id", using: :btree
- add_index "todos", ["group_id"], name: "index_todos_on_group_id", using: :btree
- add_index "todos", ["note_id"], name: "index_todos_on_note_id", using: :btree
- add_index "todos", ["project_id"], name: "index_todos_on_project_id", using: :btree
- add_index "todos", ["target_type", "target_id"], name: "index_todos_on_target_type_and_target_id", using: :btree
- add_index "todos", ["user_id", "id"], name: "index_todos_on_user_id_and_id_done", where: "((state)::text = 'done'::text)", using: :btree
- add_index "todos", ["user_id", "id"], name: "index_todos_on_user_id_and_id_pending", where: "((state)::text = 'pending'::text)", using: :btree
- add_index "todos", ["user_id"], name: "index_todos_on_user_id", using: :btree
-
create_table "trending_projects", 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
- add_index "trending_projects", ["project_id"], name: "index_trending_projects_on_project_id", unique: true, using: :btree
-
create_table "u2f_registrations", force: :cascade do |t|
t.text "certificate"
t.string "key_handle"
@@ -2111,13 +1997,12 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "name"
+ t.index ["key_handle"], name: "index_u2f_registrations_on_key_handle", using: :btree
+ t.index ["user_id"], name: "index_u2f_registrations_on_user_id", using: :btree
end
- add_index "u2f_registrations", ["key_handle"], name: "index_u2f_registrations_on_key_handle", using: :btree
- add_index "u2f_registrations", ["user_id"], name: "index_u2f_registrations_on_user_id", using: :btree
-
create_table "uploads", force: :cascade do |t|
- t.integer "size", limit: 8, null: false
+ t.bigint "size", null: false
t.string "path", limit: 511, null: false
t.string "checksum", limit: 64
t.integer "model_id"
@@ -2127,12 +2012,12 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.string "mount_point"
t.string "secret"
t.integer "store"
+ t.index ["checksum"], name: "index_uploads_on_checksum", using: :btree
+ t.index ["model_id", "model_type"], name: "index_uploads_on_model_id_and_model_type", using: :btree
+ t.index ["store"], name: "index_uploads_on_store", using: :btree
+ t.index ["uploader", "path"], name: "index_uploads_on_uploader_and_path", using: :btree
end
- add_index "uploads", ["checksum"], name: "index_uploads_on_checksum", using: :btree
- add_index "uploads", ["model_id", "model_type"], name: "index_uploads_on_model_id_and_model_type", using: :btree
- add_index "uploads", ["uploader", "path"], name: "index_uploads_on_uploader_and_path", using: :btree
-
create_table "user_agent_details", force: :cascade do |t|
t.string "user_agent", null: false
t.string "ip_address", null: false
@@ -2141,47 +2026,42 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.boolean "submitted", default: false, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.index ["subject_id", "subject_type"], name: "index_user_agent_details_on_subject_id_and_subject_type", using: :btree
end
- add_index "user_agent_details", ["subject_id", "subject_type"], name: "index_user_agent_details_on_subject_id_and_subject_type", using: :btree
-
create_table "user_callouts", 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
- add_index "user_callouts", ["user_id", "feature_name"], name: "index_user_callouts_on_user_id_and_feature_name", unique: true, using: :btree
- add_index "user_callouts", ["user_id"], name: "index_user_callouts_on_user_id", using: :btree
-
create_table "user_custom_attributes", 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
t.string "key", null: false
t.string "value", null: false
+ t.index ["key", "value"], name: "index_user_custom_attributes_on_key_and_value", using: :btree
+ t.index ["user_id", "key"], name: "index_user_custom_attributes_on_user_id_and_key", unique: true, using: :btree
end
- add_index "user_custom_attributes", ["key", "value"], name: "index_user_custom_attributes_on_key_and_value", using: :btree
- add_index "user_custom_attributes", ["user_id", "key"], name: "index_user_custom_attributes_on_user_id_and_key", unique: true, using: :btree
-
create_table "user_interacted_projects", id: false, force: :cascade do |t|
t.integer "user_id", null: false
t.integer "project_id", null: false
+ t.index ["project_id", "user_id"], name: "index_user_interacted_projects_on_project_id_and_user_id", unique: true, using: :btree
+ t.index ["user_id"], name: "index_user_interacted_projects_on_user_id", using: :btree
end
- add_index "user_interacted_projects", ["project_id", "user_id"], name: "index_user_interacted_projects_on_project_id_and_user_id", unique: true, using: :btree
- add_index "user_interacted_projects", ["user_id"], name: "index_user_interacted_projects_on_user_id", using: :btree
-
create_table "user_preferences", 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
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
+ t.index ["user_id"], name: "index_user_preferences_on_user_id", unique: true, using: :btree
end
- add_index "user_preferences", ["user_id"], name: "index_user_preferences_on_user_id", unique: true, using: :btree
-
create_table "user_statuses", primary_key: "user_id", force: :cascade do |t|
t.integer "cached_markdown_version"
t.string "emoji", default: "speech_balloon", null: false
@@ -2195,10 +2075,9 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.boolean "location_synced", default: false
t.integer "user_id", null: false
t.string "provider"
+ t.index ["user_id"], name: "index_user_synced_attributes_metadata_on_user_id", unique: true, using: :btree
end
- add_index "user_synced_attributes_metadata", ["user_id"], name: "index_user_synced_attributes_metadata_on_user_id", unique: true, using: :btree
-
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
@@ -2268,33 +2147,31 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.boolean "private_profile"
t.boolean "include_private_contributions"
t.string "commit_email"
+ t.index ["admin"], name: "index_users_on_admin", using: :btree
+ t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree
+ t.index ["created_at"], name: "index_users_on_created_at", using: :btree
+ t.index ["email"], name: "index_users_on_email", unique: true, using: :btree
+ t.index ["email"], name: "index_users_on_email_trigram", using: :gin, opclasses: {"email"=>"gin_trgm_ops"}
+ t.index ["feed_token"], name: "index_users_on_feed_token", using: :btree
+ t.index ["ghost"], name: "index_users_on_ghost", using: :btree
+ t.index ["incoming_email_token"], name: "index_users_on_incoming_email_token", using: :btree
+ t.index ["name"], name: "index_users_on_name", using: :btree
+ t.index ["name"], name: "index_users_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
+ t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
+ t.index ["state"], name: "index_users_on_state", using: :btree
+ t.index ["username"], name: "index_users_on_username", using: :btree
+ t.index ["username"], name: "index_users_on_username_trigram", using: :gin, opclasses: {"username"=>"gin_trgm_ops"}
end
- add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
- add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree
- add_index "users", ["created_at"], name: "index_users_on_created_at", using: :btree
- add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
- add_index "users", ["email"], name: "index_users_on_email_trigram", using: :gin, opclasses: {"email"=>"gin_trgm_ops"}
- add_index "users", ["feed_token"], name: "index_users_on_feed_token", using: :btree
- add_index "users", ["ghost"], name: "index_users_on_ghost", using: :btree
- add_index "users", ["incoming_email_token"], name: "index_users_on_incoming_email_token", using: :btree
- add_index "users", ["name"], name: "index_users_on_name", using: :btree
- add_index "users", ["name"], name: "index_users_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
- add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
- add_index "users", ["state"], name: "index_users_on_state", using: :btree
- add_index "users", ["username"], name: "index_users_on_username", using: :btree
- add_index "users", ["username"], name: "index_users_on_username_trigram", using: :gin, opclasses: {"username"=>"gin_trgm_ops"}
-
create_table "users_star_projects", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "user_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
+ t.index ["project_id"], name: "index_users_star_projects_on_project_id", using: :btree
+ t.index ["user_id", "project_id"], name: "index_users_star_projects_on_user_id_and_project_id", unique: true, using: :btree
end
- add_index "users_star_projects", ["project_id"], name: "index_users_star_projects_on_project_id", using: :btree
- add_index "users_star_projects", ["user_id", "project_id"], name: "index_users_star_projects_on_user_id_and_project_id", unique: true, using: :btree
-
create_table "web_hook_logs", force: :cascade do |t|
t.integer "web_hook_id", null: false
t.string "trigger"
@@ -2308,11 +2185,10 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.string "internal_error_message"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.index ["created_at", "web_hook_id"], name: "index_web_hook_logs_on_created_at_and_web_hook_id", using: :btree
+ t.index ["web_hook_id"], name: "index_web_hook_logs_on_web_hook_id", using: :btree
end
- add_index "web_hook_logs", ["created_at", "web_hook_id"], name: "index_web_hook_logs_on_created_at_and_web_hook_id", using: :btree
- add_index "web_hook_logs", ["web_hook_id"], name: "index_web_hook_logs_on_web_hook_id", using: :btree
-
create_table "web_hooks", force: :cascade do |t|
t.integer "project_id"
t.datetime "created_at"
@@ -2336,11 +2212,10 @@ ActiveRecord::Schema.define(version: 20181017001059) do
t.string "encrypted_token_iv"
t.string "encrypted_url"
t.string "encrypted_url_iv"
+ t.index ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
+ t.index ["type"], name: "index_web_hooks_on_type", using: :btree
end
- add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
- add_index "web_hooks", ["type"], name: "index_web_hooks_on_type", using: :btree
-
add_foreign_key "application_settings", "users", column: "usage_stats_set_by_user_id", name: "fk_964370041d", on_delete: :nullify
add_foreign_key "badges", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "badges", "projects", on_delete: :cascade
@@ -2395,6 +2270,7 @@ ActiveRecord::Schema.define(version: 20181017001059) do
add_foreign_key "clusters_applications_ingress", "clusters", name: "fk_753a7b41c1", on_delete: :cascade
add_foreign_key "clusters_applications_jupyter", "clusters", on_delete: :cascade
add_foreign_key "clusters_applications_jupyter", "oauth_applications", on_delete: :nullify
+ add_foreign_key "clusters_applications_knative", "clusters", on_delete: :cascade
add_foreign_key "clusters_applications_prometheus", "clusters", name: "fk_557e773639", on_delete: :cascade
add_foreign_key "clusters_applications_runners", "ci_runners", column: "runner_id", name: "fk_02de2ded36", on_delete: :nullify
add_foreign_key "clusters_applications_runners", "clusters", on_delete: :cascade
@@ -2412,9 +2288,6 @@ ActiveRecord::Schema.define(version: 20181017001059) do
add_foreign_key "fork_network_members", "projects", on_delete: :cascade
add_foreign_key "fork_networks", "projects", column: "root_project_id", name: "fk_e7b436b2b5", on_delete: :nullify
add_foreign_key "forked_project_links", "projects", column: "forked_to_project_id", name: "fk_434510edb0", on_delete: :cascade
- add_foreign_key "gcp_clusters", "projects", on_delete: :cascade
- add_foreign_key "gcp_clusters", "services", on_delete: :nullify
- add_foreign_key "gcp_clusters", "users", on_delete: :nullify
add_foreign_key "gpg_key_subkeys", "gpg_keys", on_delete: :cascade
add_foreign_key "gpg_keys", "users", on_delete: :cascade
add_foreign_key "gpg_signatures", "gpg_key_subkeys", on_delete: :nullify
@@ -2481,6 +2354,7 @@ ActiveRecord::Schema.define(version: 20181017001059) do
add_foreign_key "project_import_data", "projects", name: "fk_ffb9ee3a10", on_delete: :cascade
add_foreign_key "project_mirror_data", "projects", on_delete: :cascade
add_foreign_key "project_statistics", "projects", on_delete: :cascade
+ add_foreign_key "projects", "repositories", column: "pool_repository_id", name: "fk_6e5c14658a", on_delete: :nullify
add_foreign_key "prometheus_metrics", "projects", on_delete: :cascade
add_foreign_key "protected_branch_merge_access_levels", "protected_branches", name: "fk_8a3072ccb3", on_delete: :cascade
add_foreign_key "protected_branch_push_access_levels", "protected_branches", name: "fk_9ffc86a3d9", on_delete: :cascade
@@ -2492,6 +2366,7 @@ ActiveRecord::Schema.define(version: 20181017001059) do
add_foreign_key "push_event_payloads", "events", name: "fk_36c74129da", on_delete: :cascade
add_foreign_key "releases", "projects", name: "fk_47fe2a0596", on_delete: :cascade
add_foreign_key "remote_mirrors", "projects", on_delete: :cascade
+ add_foreign_key "repositories", "shards", on_delete: :restrict
add_foreign_key "repository_languages", "projects", on_delete: :cascade
add_foreign_key "resource_label_events", "issues", on_delete: :cascade
add_foreign_key "resource_label_events", "labels", on_delete: :nullify
diff --git a/doc/administration/auth/authentiq.md b/doc/administration/auth/authentiq.md
index 252ff1f4b15..772e55cef07 100644
--- a/doc/administration/auth/authentiq.md
+++ b/doc/administration/auth/authentiq.md
@@ -6,7 +6,7 @@ Authentiq will generate a Client ID and the accompanying Client Secret for you t
1. Get your Client credentials (Client ID and Client Secret) at [Authentiq](https://www.authentiq.com/developers).
-2. On your GitLab server, open the configuration file:
+1. On your GitLab server, open the configuration file:
For omnibus installation
```sh
@@ -18,11 +18,11 @@ Authentiq will generate a Client ID and the accompanying Client Secret for you t
```sh
sudo -u git -H editor /home/git/gitlab/config/gitlab.yml
```
-
-3. See [Initial OmniAuth Configuration](../../integration/omniauth.md#initial-omniauth-configuration) for initial settings to enable single sign-on and add Authentiq as an OAuth provider.
-4. Add the provider configuration for Authentiq:
-
+1. See [Initial OmniAuth Configuration](../../integration/omniauth.md#initial-omniauth-configuration) for initial settings to enable single sign-on and add Authentiq as an OAuth provider.
+
+1. Add the provider configuration for Authentiq:
+
For Omnibus packages:
```ruby
@@ -31,15 +31,15 @@ Authentiq will generate a Client ID and the accompanying Client Secret for you t
"name" => "authentiq",
"app_id" => "YOUR_CLIENT_ID",
"app_secret" => "YOUR_CLIENT_SECRET",
- "args" => {
+ "args" => {
"scope": 'aq:name email~rs address aq:push'
}
}
]
```
-
+
For installations from source:
-
+
```yaml
- { name: 'authentiq',
app_id: 'YOUR_CLIENT_ID',
@@ -49,20 +49,20 @@ Authentiq will generate a Client ID and the accompanying Client Secret for you t
}
}
```
-
-
-5. The `scope` is set to request the user's name, email (required and signed), and permission to send push notifications to sign in on subsequent visits.
+
+
+1. The `scope` is set to request the user's name, email (required and signed), and permission to send push notifications to sign in on subsequent visits.
See [OmniAuth Authentiq strategy](https://github.com/AuthentiqID/omniauth-authentiq/wiki/Scopes,-callback-url-configuration-and-responses) for more information on scopes and modifiers.
-6. Change `YOUR_CLIENT_ID` and `YOUR_CLIENT_SECRET` to the Client credentials you received in step 1.
+1. Change `YOUR_CLIENT_ID` and `YOUR_CLIENT_SECRET` to the Client credentials you received in step 1.
-7. Save the configuration file.
+1. Save the configuration file.
-8. [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.
+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 Authentiq icon below the regular sign in form. Click the icon to begin the authentication process.
+On the sign in page there should now be an Authentiq icon below the regular sign in form. Click the icon to begin the authentication process.
-- If the user has the Authentiq ID app installed in their iOS or Android device, they can scan the QR code, decide what personal details to share and sign in to your GitLab installation.
-- If not they will be prompted to download the app and then follow the procedure above.
+- If the user has the Authentiq ID app installed in their iOS or Android device, they can scan the QR code, decide what personal details to share and sign in to your GitLab installation.
+- If not they will be prompted to download the app and then follow the procedure above.
-If everything goes right, the user will be returned to GitLab and will be signed in. \ No newline at end of file
+If everything goes right, the user will be returned to GitLab and will be signed in.
diff --git a/doc/administration/auth/okta.md b/doc/administration/auth/okta.md
index 664657650d4..ae38094391b 100644
--- a/doc/administration/auth/okta.md
+++ b/doc/administration/auth/okta.md
@@ -11,7 +11,7 @@ The following documentation enables Okta as a SAML provider.
1. When the app screen comes up you see another button to **Create an App** and
choose SAML 2.0 on the next screen.
1. Now, very important, add a logo
- (you can choose it from https://about.gitlab.com/press/). You'll have to
+ (you can choose it from <https://about.gitlab.com/press/>). You'll have to
crop and resize it.
1. Next, you'll need the to fill in the SAML general config. Here's an example
image.
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
index 890b780fe80..cfe7b0e05e3 100644
--- a/doc/administration/container_registry.md
+++ b/doc/administration/container_registry.md
@@ -71,7 +71,7 @@ A Registry init file is not shipped with GitLab if you install it from source.
Hence, [restarting GitLab][restart gitlab] will not restart the Registry should
you modify its settings. Read the upstream documentation on how to achieve that.
-At the absolute minimum, make sure your [Registry configuration][registry-auth]
+At the **absolute** minimum, make sure your [Registry configuration][registry-auth]
has `container_registry` as the service and `https://gitlab.example.com/jwt/auth`
as the realm:
@@ -84,6 +84,9 @@ auth:
rootcertbundle: /root/certs/certbundle
```
+CAUTION: **Caution:**
+If `auth` is not set up, users will be able to pull docker images without authentication.
+
## Container Registry domain configuration
There are two ways you can configure the Registry's external domain.
diff --git a/doc/administration/custom_hooks.md b/doc/administration/custom_hooks.md
index c58ced7d520..60ad4bf4e8f 100644
--- a/doc/administration/custom_hooks.md
+++ b/doc/administration/custom_hooks.md
@@ -60,7 +60,7 @@ installations, this can be set in `gitlab-shell/config.yml`.
The hooks are searched and executed in this order:
-1. `<project>.git/hooks/` - symlink to `gitlab-shell/hooks` global dir
+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/custom_hooks/<hook_name>` - per project hook (this is already existing behavior)
1. `<project>.git/custom_hooks/<hook_name>.d/*` - per project hooks
diff --git a/doc/administration/git_protocol.md b/doc/administration/git_protocol.md
index 6b82771baf9..b1be078d672 100644
--- a/doc/administration/git_protocol.md
+++ b/doc/administration/git_protocol.md
@@ -4,9 +4,7 @@ description: "Set and configure Git protocol v2"
# Configuring Git Protocol v2
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/46555) in GitLab 11.4.
-
----
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/46555) in GitLab 11.4.
Git protocol v2 improves the v1 wire protocol in several ways and is
enabled by default in GitLab for HTTP requests. In order to enable SSH,
diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md
index 481eb692674..74b0e2c8184 100644
--- a/doc/administration/high_availability/nfs.md
+++ b/doc/administration/high_availability/nfs.md
@@ -59,7 +59,7 @@ on an Linux NFS server, do the following:
sysctl -w fs.leases-enable=0
```
-2. Restart the NFS server process. For example, on CentOS run `service nfs restart`.
+1. Restart the NFS server process. For example, on CentOS run `service nfs restart`.
## Avoid using AWS's Elastic File System (EFS)
@@ -87,12 +87,12 @@ this configuration.
Additionally, this configuration is specifically warned against in the
[Postgres Documentation](https://www.postgresql.org/docs/current/static/creating-cluster.html#CREATING-CLUSTER-NFS):
->PostgreSQL does nothing special for NFS file systems, meaning it assumes NFS behaves exactly like
->locally-connected drives. If the client or server NFS implementation does not provide standard file
->system semantics, this can cause reliability problems. Specifically, delayed (asynchronous) writes
+>PostgreSQL does nothing special for NFS file systems, meaning it assumes NFS behaves exactly like
+>locally-connected drives. If the client or server NFS implementation does not provide standard file
+>system semantics, this can cause reliability problems. Specifically, delayed (asynchronous) writes
>to the NFS server can cause data corruption problems.
-For supported database architecture, please see our documentation on
+For supported database architecture, please see our documentation on
[Configuring a Database for GitLab HA](https://docs.gitlab.com/ee/administration/high_availability/database.html).
## NFS Client mount options
diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md
index dcee57def74..7c1ef43499d 100644
--- a/doc/administration/high_availability/redis.md
+++ b/doc/administration/high_availability/redis.md
@@ -665,7 +665,7 @@ cache, queues, and shared_state. To make this work with Sentinel:
**Note**: Redis URLs should be in the format: `redis://:PASSWORD@SENTINEL_MASTER_NAME`
1. PASSWORD is the plaintext password for the Redis instance
- 2. SENTINEL_MASTER_NAME is the Sentinel master name (e.g. `gitlab-redis-cache`)
+ 1. SENTINEL_MASTER_NAME is the Sentinel master name (e.g. `gitlab-redis-cache`)
1. Include an array of hashes with host/port combinations, such as the following:
```ruby
diff --git a/doc/administration/logs.md b/doc/administration/logs.md
index 038e043281c..7e5a3eb9ccd 100644
--- a/doc/administration/logs.md
+++ b/doc/administration/logs.md
@@ -29,9 +29,9 @@ Each line contains a JSON line that can be ingested by Elasticsearch, Splunk, et
In this example, you can see this was a GET request for a specific issue. Notice each line also contains performance data:
1. `duration`: the total time taken to retrieve the request
-2. `view`: total time taken inside the Rails views
-3. `db`: total time to retrieve data from the database
-4. `gitaly_calls`: total number of calls made to Gitaly
+1. `view`: total time taken inside the Rails views
+1. `db`: total time to retrieve data from the database
+1. `gitaly_calls`: total number of calls made to Gitaly
User clone/fetch activity using http transport appears in this log as `action: git_upload_pack`.
@@ -119,7 +119,7 @@ This file lives in `/var/log/gitlab/gitlab-rails/integrations_json.log` for
Omnibus GitLab packages or in `/home/git/gitlab/log/integrations_json.log` for
installations from source.
-It contains information about [integrations](../user/project/integrations/project_services.md) activities such as JIRA, Asana and Irker services. It uses JSON format like the example below:
+It contains information about [integrations](../user/project/integrations/project_services.md) activities such as JIRA, Asana and Irker services. It uses JSON format like the example below:
``` json
{"severity":"ERROR","time":"2018-09-06T14:56:20.439Z","service_class":"JiraService","project_id":8,"project_path":"h5bp/html5-boilerplate","message":"Error sending message","client_url":"http://jira.gitlap.com:8080","error":"execution expired"}
@@ -257,8 +257,8 @@ importer. Future importers may use this file.
## Reconfigure Logs
-Reconfigure log files live in `/var/log/gitlab/reconfigure` for Omnibus GitLab
-packages. Installations from source don't have reconfigure logs. A reconfigure log
+Reconfigure log files live in `/var/log/gitlab/reconfigure` for Omnibus GitLab
+packages. Installations from source don't have reconfigure logs. A reconfigure log
is populated whenever `gitlab-ctl reconfigure` is run manually or as part of an upgrade.
Reconfigure logs files are named according to the UNIX timestamp of when the reconfigure
diff --git a/doc/administration/monitoring/performance/influxdb_configuration.md b/doc/administration/monitoring/performance/influxdb_configuration.md
index c30cd2950d8..fa281f47ed8 100644
--- a/doc/administration/monitoring/performance/influxdb_configuration.md
+++ b/doc/administration/monitoring/performance/influxdb_configuration.md
@@ -95,10 +95,10 @@ UDP can be done using the following settings:
This does the following:
1. Enable UDP and bind it to port 8089 for all addresses.
-2. Store any data received in the "gitlab" database.
-3. Define a batch of points to be 1000 points in size and allow a maximum of
+1. Store any data received in the "gitlab" database.
+1. Define a batch of points to be 1000 points in size and allow a maximum of
5 batches _or_ flush them automatically after 1 second.
-4. Define a UDP read buffer size of 200 MB.
+1. Define a UDP read buffer size of 200 MB.
One of the most important settings here is the UDP read buffer size as if this
value is set too low, packets will be dropped. You must also make sure the OS
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index c6fd7ef7360..c9a2778b3a4 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -45,6 +45,7 @@ The following metrics are available:
| redis_ping_success | Gauge | 9.4 | Whether or not the last redis ping succeeded |
| redis_ping_latency_seconds | Gauge | 9.4 | Round trip time of the redis ping |
| user_session_logins_total | Counter | 9.4 | Counter of how many users have logged in |
+| upload_file_does_not_exist | Counter | 10.7 in EE, 11.5 in CE | Number of times an upload record could not find its file |
| failed_login_captcha_total | Gauge | 11.0 | Counter of failed CAPTCHA attempts during login |
| successful_login_captcha_total | Gauge | 11.0 | Counter of successful CAPTCHA attempts during login |
diff --git a/doc/administration/monitoring/prometheus/index.md b/doc/administration/monitoring/prometheus/index.md
index b1b670c3b42..33611c5efc3 100644
--- a/doc/administration/monitoring/prometheus/index.md
+++ b/doc/administration/monitoring/prometheus/index.md
@@ -1,4 +1,4 @@
-# GitLab Prometheus
+# Monitoring GitLab with Prometheus
> **Notes:**
> - Prometheus and the various exporters listed in this page are bundled in the
@@ -24,7 +24,7 @@ dashboard tool like [Grafana].
## Configuring Prometheus
->**Note:**
+NOTE: **Note:**
For installations from source you'll have to install and configure it yourself.
Prometheus and it's exporters are on by default, starting with GitLab 9.0.
@@ -43,17 +43,17 @@ To disable Prometheus and all of its exporters, as well as any added in the futu
```
1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
- take effect
+ take effect.
-## Changing the port and address Prometheus listens on
+### Changing the port and address Prometheus listens on
->**Note:**
+NOTE: **Note:**
The following change was added in [GitLab Omnibus 8.17][1261]. Although possible,
it's not recommended to change the port Prometheus listens
on as this might affect or conflict with other services running on the GitLab
server. Proceed at your own risk.
-In order to access Prometheus from outside the GitLab server you will need to
+In order to access Prometheus from outside the GitLab server you will need to
set a FQDN or IP in `prometheus['listen_address']`.
To change the address/port that Prometheus listens on:
@@ -77,6 +77,60 @@ To change the address/port that Prometheus listens on:
1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
take effect
+### Using an external Prometheus server
+
+NOTE: **Note:**
+Prometheus and most exporters do not support authentication. We do not recommend exposing them outside the local network.
+
+A few configuration changes are required to allow GitLab to be monitored by an external Prometheus server. External servers are recommended for highly available deployments of GitLab with multiple nodes.
+
+To use an external Prometheus server:
+
+1. Edit `/etc/gitlab/gitlab.rb`.
+1. Disable the bundled Prometheus:
+
+ ```ruby
+ prometheus['enable'] = false
+ ```
+
+1. Set each bundled service's [exporter](#bundled-software-metrics) to listen on a network address, for example:
+
+ ```ruby
+ gitlab_monitor['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'
+ ```
+
+1. Install and set up a dedicated Prometheus instance, if necessary, using the [official installation instructions](https://prometheus.io/docs/prometheus/latest/installation/).
+1. Add the Prometheus server IP address to the [monitoring IP whitelist](../ip_whitelist.html). For example:
+
+ ```ruby
+ gitlab_rails['monitoring_whitelist'] = ['127.0.0.0/8', '192.168.0.1']
+ ```
+
+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
+ [scrape target configuration](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#%3Cscrape_config%3E).
+ For example, a sample snippet using `static_configs`:
+
+ ```yaml
+ scrape_configs:
+ - job_name: 'gitlab_exporters'
+ static_configs:
+ - targets: ['1.1.1.1:9168', '1.1.1.1:9236', '1.1.1.1:9236', '1.1.1.1:9100', '1.1.1.1:9121', '1.1.1.1:9187']
+
+ - job_name: 'gitlab_metrics'
+ metrics_path: /-/metrics
+ static_configs:
+ - targets: ['1.1.1.1:443']
+ ```
+
+1. Restart the Prometheus server.
+
## Viewing performance metrics
You can visit `http://localhost:9090` for the dashboard that Prometheus offers by default.
@@ -86,7 +140,7 @@ If SSL has been enabled on your GitLab instance, you may not be able to access
Prometheus on the same browser as GitLab if using the same FQDN due to [HSTS][hsts]. We plan to
[provide access via GitLab][multi-user-prometheus], but in the interim there are
some workarounds: using a separate FQDN, using server IP, using a separate browser for Prometheus, resetting HSTS, or
-having [Nginx proxy it][nginx-custom-config].
+having [NGINX proxy it][nginx-custom-config].
The performance data collected by Prometheus can be viewed directly in the
Prometheus console or through a compatible dashboard tool.
@@ -102,26 +156,7 @@ Sample Prometheus queries:
- **Data transmitted:** `rate(node_network_transmit_bytes_total{device!="lo"}[5m])`
- **Data received:** `rate(node_network_receive_bytes_total{device!="lo"}[5m])`
-## Configuring Prometheus to monitor Kubernetes
-
-> Introduced in GitLab 9.0.
-> Pod monitoring introduced in GitLab 9.4.
-
-If your GitLab server is running within Kubernetes, Prometheus will collect metrics from the Nodes and [annotated Pods](https://prometheus.io/docs/operating/configuration/#kubernetes_sd_config) in the cluster, including performance data on each container. This is particularly helpful if your CI/CD environments run in the same cluster, as you can use the [Prometheus project integration][] to monitor them.
-
-To disable the monitoring of Kubernetes:
-
-1. Edit `/etc/gitlab/gitlab.rb`
-1. Add or find and uncomment the following line and set it to `false`:
-
- ```ruby
- prometheus['monitor_kubernetes'] = false
- ```
-
-1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
- take effect
-
-## GitLab Prometheus metrics
+## GitLab metrics
> Introduced in GitLab 9.3.
@@ -129,17 +164,10 @@ GitLab monitors its own internal service metrics, and makes them available at th
[➔ Read more about the GitLab Metrics.](gitlab_metrics.md)
-## Prometheus exporters
-
-There are a number of libraries and servers which help in exporting existing
-metrics from third-party systems as Prometheus metrics. This is useful for cases
-where it is not feasible to instrument a given system with Prometheus metrics
-directly (for example, HAProxy or Linux system stats). You can read more in the
-[Prometheus exporters and integrations upstream documentation][prom-exporters].
+## Bundled software metrics
-While you can use any exporter you like with your GitLab installation, the
-following ones documented here are bundled in the Omnibus GitLab packages
-making it easy to configure and use.
+Many of the GitLab dependencies bundled in Omnibus GitLab are preconfigured to
+export Prometheus metrics.
### Node exporter
@@ -166,6 +194,25 @@ The GitLab monitor exporter allows you to measure various GitLab metrics, pulled
[➔ Read more about the GitLab monitor exporter.](gitlab_monitor_exporter.md)
+## Configuring Prometheus to monitor Kubernetes
+
+> Introduced in GitLab 9.0.
+> Pod monitoring introduced in GitLab 9.4.
+
+If your GitLab server is running within Kubernetes, Prometheus will collect metrics from the Nodes and [annotated Pods](https://prometheus.io/docs/operating/configuration/#kubernetes_sd_config) in the cluster, including performance data on each container. This is particularly helpful if your CI/CD environments run in the same cluster, as you can use the [Prometheus project integration][prometheus integration] to monitor them.
+
+To disable the monitoring of Kubernetes:
+
+1. Edit `/etc/gitlab/gitlab.rb`.
+1. Add or find and uncomment the following line and set it to `false`:
+
+ ```ruby
+ prometheus['monitor_kubernetes'] = false
+ ```
+
+1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
+ take effect.
+
[grafana]: https://grafana.net
[hsts]: https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security
[multi-user-prometheus]: https://gitlab.com/gitlab-org/multi-user-prometheus
diff --git a/doc/administration/operations/fast_ssh_key_lookup.md b/doc/administration/operations/fast_ssh_key_lookup.md
index eada7b19dcd..c293df3fc57 100644
--- a/doc/administration/operations/fast_ssh_key_lookup.md
+++ b/doc/administration/operations/fast_ssh_key_lookup.md
@@ -5,7 +5,7 @@ NOTE: **Note:** This document describes a drop-in replacement for the
using [ssh certificates](ssh_certificates.md), they are even faster,
but are not a drop-in replacement.
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/1631) in
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/1631) in
> [GitLab Starter](https://about.gitlab.com/gitlab-ee) 9.3.
>
> [Available in](https://gitlab.com/gitlab-org/gitlab-ee/issues/3953) GitLab
@@ -109,7 +109,7 @@ the database. The following instructions can be used to build OpenSSH 7.5:
yum install rpm-build gcc make wget openssl-devel krb5-devel pam-devel libX11-devel xmkmf libXt-devel
```
-3. Prepare the build by copying files to the right place:
+1. Prepare the build by copying files to the right place:
```
mkdir -p /root/rpmbuild/{SOURCES,SPECS}
@@ -118,7 +118,7 @@ the database. The following instructions can be used to build OpenSSH 7.5:
cd /root/rpmbuild/SPECS
```
-3. Next, set the spec settings properly:
+1. Next, set the spec settings properly:
```
sed -i -e "s/%define no_gnome_askpass 0/%define no_gnome_askpass 1/g" openssh.spec
@@ -126,13 +126,13 @@ the database. The following instructions can be used to build OpenSSH 7.5:
sed -i -e "s/BuildPreReq/BuildRequires/g" openssh.spec
```
-3. Build the RPMs:
+1. Build the RPMs:
```
rpmbuild -bb openssh.spec
```
-4. Ensure the RPMs were built:
+1. Ensure the RPMs were built:
```
ls -al /root/rpmbuild/RPMS/x86_64/
@@ -150,7 +150,7 @@ the database. The following instructions can be used to build OpenSSH 7.5:
-rw-r--r--. 1 root root 367516 Jun 20 19:37 openssh-server-7.5p1-1.x86_64.rpm
```
-5. Install the packages. OpenSSH packages will replace `/etc/pam.d/sshd`
+1. Install the packages. OpenSSH packages will replace `/etc/pam.d/sshd`
with its own version, which may prevent users from logging in, so be sure
that the file is backed up and restored after installation:
@@ -161,7 +161,7 @@ the database. The following instructions can be used to build OpenSSH 7.5:
yes | cp pam-ssh-conf-$timestamp /etc/pam.d/sshd
```
-6. Verify the installed version. In another window, attempt to login to the server:
+1. Verify the installed version. In another window, attempt to login to the server:
```
ssh -v <your-centos-machine>
@@ -171,7 +171,7 @@ the database. The following instructions can be used to build OpenSSH 7.5:
If not, you may need to restart sshd (e.g. `systemctl restart sshd.service`).
-7. *IMPORTANT!* Open a new SSH session to your server before exiting to make
+1. *IMPORTANT!* Open a new SSH session to your server before exiting to make
sure everything is working! If you need to downgrade, simple install the
older package:
diff --git a/doc/administration/raketasks/maintenance.md b/doc/administration/raketasks/maintenance.md
index 29af07d12dc..0d863594fc7 100644
--- a/doc/administration/raketasks/maintenance.md
+++ b/doc/administration/raketasks/maintenance.md
@@ -53,6 +53,7 @@ Git: /usr/bin/git
Runs the following rake tasks:
- `gitlab:gitlab_shell:check`
+- `gitlab:gitaly:check`
- `gitlab:sidekiq:check`
- `gitlab:app:check`
@@ -252,7 +253,7 @@ clear it.
To clear all exclusive leases:
-DANGER: **DANGER**:
+DANGER: **DANGER**:
Don't run it while GitLab or Sidekiq is running
```bash
diff --git a/doc/administration/reply_by_email_postfix_setup.md b/doc/administration/reply_by_email_postfix_setup.md
index d1a03219542..4c42cb7756a 100644
--- a/doc/administration/reply_by_email_postfix_setup.md
+++ b/doc/administration/reply_by_email_postfix_setup.md
@@ -8,7 +8,7 @@ The instructions make the assumption that you will be using the email address `i
## Configure your server firewall
1. Open up port 25 on your server so that people can send email into the server over SMTP.
-2. If the mail server is different from the server running GitLab, open up port 143 on your server so that GitLab can read email from the server over IMAP.
+1. If the mail server is different from the server running GitLab, open up port 143 on your server so that GitLab can read email from the server over IMAP.
## Install packages
diff --git a/doc/administration/troubleshooting/debug.md b/doc/administration/troubleshooting/debug.md
index 2902af8c782..643c5b9fe80 100644
--- a/doc/administration/troubleshooting/debug.md
+++ b/doc/administration/troubleshooting/debug.md
@@ -20,7 +20,7 @@ an SMTP server, but you're not seeing mail delivered. Here's how to check the se
bundle exec rails console production
```
-2. Look at the ActionMailer `delivery_method` to make sure it matches what you
+1. Look at the ActionMailer `delivery_method` to make sure it matches what you
intended. If you configured SMTP, it should say `:smtp`. If you're using
Sendmail, it should say `:sendmail`:
@@ -29,7 +29,7 @@ an SMTP server, but you're not seeing mail delivered. Here's how to check the se
=> :smtp
```
-3. If you're using SMTP, check the mail settings:
+1. If you're using SMTP, check the mail settings:
```ruby
irb(main):002:0> ActionMailer::Base.smtp_settings
@@ -39,7 +39,7 @@ an SMTP server, but you're not seeing mail delivered. Here's how to check the se
In the example above, the SMTP server is configured for the local machine. If this is intended, you may need to check your local mail
logs (e.g. `/var/log/mail.log`) for more details.
-4. Send a test message via the console.
+1. Send a test message via the console.
```ruby
irb(main):003:0> Notify.test_email('youremail@email.com', 'Hello World', 'This is a test message').deliver_now
diff --git a/doc/api/README.md b/doc/api/README.md
index a620a13a3b3..19abbdc7a1e 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -61,6 +61,7 @@ following locations:
- [Protected Tags](protected_tags.md)
- [Repositories](repositories.md)
- [Repository Files](repository_files.md)
+- [Repository Submodules](repository_submodules.md)
- [Runners](runners.md)
- [Search](search.md)
- [Services](services.md)
@@ -234,7 +235,7 @@ provided you are authenticated as an administrator with an OAuth or Personal Acc
You need to pass the `sudo` parameter either via query string or a header with an ID/username of
the user you want to perform the operation as. If passed as a header, the
-header name must be `Sudo`.
+header name must be `Sudo`.
NOTE: **Note:**
Usernames are case insensitive.
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 9b7ca4b6e70..994eefa423f 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -288,6 +288,47 @@ Example response:
}
```
+## Revert a commit
+
+> [Introduced][ce-22919] in GitLab 11.6.
+
+Reverts a commit in a given branch.
+
+```
+POST /projects/:id/repository/commits/:sha/revert
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `sha` | string | yes | Commit SHA to revert |
+| `branch` | string | yes | Target branch name |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "branch=master" "https://gitlab.example.com/api/v4/projects/5/repository/commits/a738f717824ff53aebad8b090c1b79a14f2bd9e8/revert"
+```
+
+Example response:
+
+```json
+{
+ "id":"8b090c1b79a14f2bd9e8a738f717824ff53aebad",
+ "short_id": "8b090c1b",
+ "title":"Revert \"Feature added\"",
+ "created_at":"2018-11-08T15:55:26.000Z",
+ "parent_ids":["a738f717824ff53aebad8b090c1b79a14f2bd9e8"],
+ "message":"Revert \"Feature added\"\n\nThis reverts commit a738f717824ff53aebad8b090c1b79a14f2bd9e8",
+ "author_name":"Administrator",
+ "author_email":"admin@example.com",
+ "authored_date":"2018-11-08T15:55:26.000Z",
+ "committer_name":"Administrator",
+ "committer_email":"admin@example.com",
+ "committed_date":"2018-11-08T15:55:26.000Z"
+}
+```
+
## Get the diff of a commit
Get the diff of a commit in a project.
@@ -619,3 +660,4 @@ Example response:
[ce-8047]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8047
[ce-15026]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15026
[ce-18004]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18004
+[ce-22919]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22919
diff --git a/doc/api/events.md b/doc/api/events.md
index ccac5b8bb60..e1c6b801a77 100644
--- a/doc/api/events.md
+++ b/doc/api/events.md
@@ -71,7 +71,7 @@ Parameters:
Example request:
```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/events&target_type=issue&action=created&after=2017-01-31&before=2017-03-01
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/events?target_type=issue&action=created&after=2017-01-31&before=2017-03-01
```
Example response:
@@ -276,7 +276,7 @@ Parameters:
Example request:
```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:project_id/events&target_type=issue&action=created&after=2017-01-31&before=2017-03-01
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:project_id/events?target_type=issue&action=created&after=2017-01-31&before=2017-03-01
```
Example response:
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 57e861bc62e..0dc9d706120 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -37,11 +37,11 @@ GET /issues?my_reaction_emoji=star
| ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `state` | string | no | Return all issues or just those that are `opened` or `closed` |
| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels |
-| `milestone` | string | no | The milestone title. `No+Milestone` lists all issues with no milestone. `Any+Milestone` lists all issues that have an assigned milestone |
+| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. |
| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ |
| `author_id` | integer | no | Return issues created by the given user `id`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ |
| `assignee_id` | integer | no | Return issues assigned to the given user `id`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ |
-| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
+| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ |
| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` |
| `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` |
@@ -151,11 +151,11 @@ GET /groups/:id/issues?my_reaction_emoji=star
| `state` | string | no | Return all issues or just those that are `opened` or `closed` |
| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels |
| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` |
-| `milestone` | string | no | The milestone title. `No+Milestone` lists all issues with no milestone |
+| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. |
| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ |
| `author_id` | integer | no | Return issues created by the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ |
| `assignee_id` | integer | no | Return issues assigned to the given user `id`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ |
-| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
+| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ |
| `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Search group issues against their `title` and `description` |
@@ -265,11 +265,11 @@ GET /projects/:id/issues?my_reaction_emoji=star
| `iids[]` | Array[integer] | no | Return only the milestone having the given `iid` |
| `state` | string | no | Return all issues or just those that are `opened` or `closed` |
| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels |
-| `milestone` | string | no | The milestone title. `No+Milestone` lists all issues with no milestone |
+| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. |
| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ |
| `author_id` | integer | no | Return issues created by the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ |
| `assignee_id` | integer | no | Return issues assigned to the given user `id`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ |
-| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
+| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ |
| `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Search project issues against their `title` and `description` |
@@ -1113,6 +1113,93 @@ Example response:
}
```
+## List merge requests related to issue
+
+Get all the merge requests that are related to the issue.
+
+```
+GET /projects/:id/issues/:issue_id/related_merge_requests
+```
+
+| Attribute | Type | Required | Description |
+|-------------|---------|----------|--------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `issue_iid` | integer | yes | The internal ID of a project's issue |
+
+```sh
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/11/related_merge_requests
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 29,
+ "iid": 11,
+ "project_id": 1,
+ "title": "Provident eius eos blanditiis consequatur neque odit.",
+ "description": "Ut consequatur ipsa aspernatur quisquam voluptatum fugit. Qui harum corporis quo fuga ut incidunt veritatis. Autem necessitatibus et harum occaecati nihil ea.\r\n\r\ntwitter/flight#8",
+ "state": "opened",
+ "created_at": "2018-09-18T14:36:15.510Z",
+ "updated_at": "2018-09-19T07:45:13.089Z",
+ "target_branch": "v2.x",
+ "source_branch": "so_long_jquery",
+ "upvotes": 0,
+ "downvotes": 0,
+ "author": {
+ "id": 14,
+ "name": "Verna Hills",
+ "username": "lawanda_reinger",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/de68a91aeab1cff563795fb98a0c2cc0?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/lawanda_reinger"
+ },
+ "assignee": {
+ "id": 19,
+ "name": "Jody Baumbach",
+ "username": "felipa.kuvalis",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/6541fc75fc4e87e203529bd275fafd07?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/felipa.kuvalis"
+ },
+ "source_project_id": 1,
+ "target_project_id": 1,
+ "labels": [],
+ "work_in_progress": false,
+ "milestone": {
+ "id": 27,
+ "iid": 2,
+ "project_id": 1,
+ "title": "v1.0",
+ "description": "Et tenetur voluptatem minima doloribus vero dignissimos vitae.",
+ "state": "active",
+ "created_at": "2018-09-18T14:35:44.353Z",
+ "updated_at": "2018-09-18T14:35:44.353Z",
+ "due_date": null,
+ "start_date": null,
+ "web_url": "https://gitlab.example.com/twitter/flight/milestones/2"
+ },
+ "merge_when_pipeline_succeeds": false,
+ "merge_status": "cannot_be_merged",
+ "sha": "3b7b528e9353295c1c125dad281ac5b5deae5f12",
+ "merge_commit_sha": null,
+ "user_notes_count": 9,
+ "discussion_locked": null,
+ "should_remove_source_branch": null,
+ "force_remove_source_branch": false,
+ "web_url": "https://gitlab.example.com/twitter/flight/merge_requests/4",
+ "time_stats": {
+ "time_estimate": 0,
+ "total_time_spent": 0,
+ "human_time_estimate": null,
+ "human_total_time_spent": null
+ },
+ "squash": false
+ }
+]
+```
+
## List merge requests that will close issue on merge
Get all the merge requests that will close issue when merged.
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 0291b7e00c2..9cb3f0d9c0c 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -30,10 +30,10 @@ Parameters:
| Attribute | Type | Required | Description |
| ------------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------- |
-| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged` |
+| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged` |
| `order_by` | string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
-| `milestone` | string | no | Return merge requests for a specific milestone |
+| `milestone` | string | no | Return merge requests for a specific milestone. `None` returns merge requests with no milestone. `Any` returns merge requests that have an assigned milestone. |
| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request |
| `labels` | string | no | Return merge requests matching a comma separated list of labels |
| `created_after` | datetime | no | Return merge requests created on or after the given time |
@@ -43,11 +43,11 @@ Parameters:
| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead. |
| `author_id` | integer | no | Returns merge requests created by the given user `id`. Combine with `scope=all` or `scope=assigned_to_me` |
| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. |
-| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
+| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ |
| `source_branch` | string | no | Return merge requests with the given source branch |
| `target_branch` | string | no | Return merge requests with the given target branch |
| `search` | string | no | Search merge requests against their `title` and `description` |
-| `wip` | string | no | Filter merge requests against their `wip` status. `yes` to return *only* WIP merge requests, `no` to return *non* WIP merge requests |
+| `wip` | string | no | Filter merge requests against their `wip` status. `yes` to return *only* WIP merge requests, `no` to return *non* WIP merge requests |
```json
[
@@ -57,7 +57,18 @@ Parameters:
"project_id": 3,
"title": "test1",
"description": "fixed login page css paddings",
- "state": "opened",
+ "state": "merged",
+ "merged_by": {
+ "id": 87854,
+ "name": "Douwe Maan",
+ "username": "DouweM",
+ "state": "active",
+ "avatar_url": "https://gitlab.example.com/uploads/-/system/user/avatar/87854/avatar.png",
+ "web_url": "https://gitlab.com/DouweM"
+ },
+ "merged_at": "2018-09-07T11:16:17.520Z",
+ "closed_by": null,
+ "closed_at": null,
"created_at": "2017-04-29T08:46:00Z",
"updated_at": "2017-04-29T08:46:00Z",
"target_branch": "master",
@@ -154,10 +165,10 @@ Parameters:
| ------------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `id` | integer | yes | The ID of a project |
| `iids[]` | Array[integer] | no | Return the request having the given `iid` |
-| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged` |
+| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged` |
| `order_by` | string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
-| `milestone` | string | no | Return merge requests for a specific milestone |
+| `milestone` | string | no | Return merge requests for a specific milestone. `None` returns merge requests with no milestone. `Any` returns merge requests that have an assigned milestone. |
| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request |
| `labels` | string | no | Return merge requests matching a comma separated list of labels |
| `created_after` | datetime | no | Return merge requests created on or after the given time |
@@ -167,9 +178,9 @@ Parameters:
| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13060] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ |
| `author_id` | integer | no | Returns merge requests created by the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ |
| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. _([Introduced][ce-13060] in GitLab 9.5)_ |
-| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
-| `source_branch` | string | no | Return merge requests with the given source branch |
-| `target_branch` | string | no | Return merge requests with the given target branch |
+| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ |
+| `source_branch` | string | no | Return merge requests with the given source branch |
+| `target_branch` | string | no | Return merge requests with the given target branch |
| `search` | string | no | Search merge requests against their `title` and `description` |
```json
@@ -180,7 +191,18 @@ Parameters:
"project_id": 3,
"title": "test1",
"description": "fixed login page css paddings",
- "state": "opened",
+ "state": "merged",
+ "merged_by": {
+ "id": 87854,
+ "name": "Douwe Maan",
+ "username": "DouweM",
+ "state": "active",
+ "avatar_url": "https://gitlab.example.com/uploads/-/system/user/avatar/87854/avatar.png",
+ "web_url": "https://gitlab.com/DouweM"
+ },
+ "merged_at": "2018-09-07T11:16:17.520Z",
+ "closed_by": null,
+ "closed_at": null,
"created_at": "2017-04-29T08:46:00Z",
"updated_at": "2017-04-29T08:46:00Z",
"target_branch": "master",
@@ -266,11 +288,11 @@ Parameters:
| Attribute | Type | Required | Description |
| ------------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ |
-| `id` | integer | yes | The ID of a group |
-| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged` |
-| `order_by` | string | no | Return merge requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
-| `sort` | string | no | Return merge requests sorted in `asc` or `desc` order. Default is `desc` |
-| `milestone` | string | no | Return merge requests for a specific milestone |
+| `id` | integer | yes | The ID of a group |
+| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged` |
+| `order_by` | string | no | Return merge requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
+| `sort` | string | no | Return merge requests sorted in `asc` or `desc` order. Default is `desc` |
+| `milestone` | string | no | Return merge requests for a specific milestone. `None` returns merge requests with no milestone. `Any` returns merge requests that have an assigned milestone. |
| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request |
| `labels` | string | no | Return merge requests matching a comma separated list of labels |
| `created_after` | datetime | no | Return merge requests created on or after the given time |
@@ -280,9 +302,9 @@ Parameters:
| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> |
| `author_id` | integer | no | Returns merge requests created by the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ |
| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. _([Introduced][ce-13060] in GitLab 9.5)_ |
-| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
-| `source_branch` | string | no | Return merge requests with the given source branch |
-| `target_branch` | string | no | Return merge requests with the given target branch |
+| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ |
+| `source_branch` | string | no | Return merge requests with the given source branch |
+| `target_branch` | string | no | Return merge requests with the given target branch |
| `search` | string | no | Search merge requests against their `title` and `description` |
```json
@@ -293,7 +315,18 @@ Parameters:
"project_id": 3,
"title": "test1",
"description": "fixed login page css paddings",
- "state": "opened",
+ "state": "merged",
+ "merged_by": {
+ "id": 87854,
+ "name": "Douwe Maan",
+ "username": "DouweM",
+ "state": "active",
+ "avatar_url": "https://gitlab.example.com/uploads/-/system/user/avatar/87854/avatar.png",
+ "web_url": "https://gitlab.com/DouweM"
+ },
+ "merged_at": "2018-09-07T11:16:17.520Z",
+ "closed_by": null,
+ "closed_at": null,
"created_at": "2017-04-29T08:46:00Z",
"updated_at": "2017-04-29T08:46:00Z",
"target_branch": "master",
@@ -383,7 +416,7 @@ Parameters:
"project_id": 3,
"title": "test1",
"description": "fixed login page css paddings",
- "state": "opened",
+ "state": "merged",
"created_at": "2017-04-29T08:46:00Z",
"updated_at": "2017-04-29T08:46:00Z",
"target_branch": "master",
@@ -695,7 +728,7 @@ POST /projects/:id/merge_requests
"project_id": 3,
"title": "test1",
"description": "fixed login page css paddings",
- "state": "opened",
+ "state": "merged",
"created_at": "2017-04-29T08:46:00Z",
"updated_at": "2017-04-29T08:46:00Z",
"target_branch": "master",
@@ -822,7 +855,7 @@ Must include at least one non-required attribute from above.
"project_id": 3,
"title": "test1",
"description": "fixed login page css paddings",
- "state": "opened",
+ "state": "merged",
"created_at": "2017-04-29T08:46:00Z",
"updated_at": "2017-04-29T08:46:00Z",
"target_branch": "master",
@@ -965,7 +998,7 @@ Parameters:
"project_id": 3,
"title": "test1",
"description": "fixed login page css paddings",
- "state": "opened",
+ "state": "merged",
"created_at": "2017-04-29T08:46:00Z",
"updated_at": "2017-04-29T08:46:00Z",
"target_branch": "master",
@@ -1080,7 +1113,7 @@ Parameters:
"project_id": 3,
"title": "test1",
"description": "fixed login page css paddings",
- "state": "opened",
+ "state": "merged",
"created_at": "2017-04-29T08:46:00Z",
"updated_at": "2017-04-29T08:46:00Z",
"target_branch": "master",
@@ -1279,7 +1312,7 @@ Example response:
"project_id": 3,
"title": "test1",
"description": "fixed login page css paddings",
- "state": "opened",
+ "state": "merged",
"created_at": "2017-04-29T08:46:00Z",
"updated_at": "2017-04-29T08:46:00Z",
"target_branch": "master",
@@ -1400,7 +1433,7 @@ Example response:
"project_id": 3,
"title": "test1",
"description": "fixed login page css paddings",
- "state": "opened",
+ "state": "merged",
"created_at": "2017-04-29T08:46:00Z",
"updated_at": "2017-04-29T08:46:00Z",
"target_branch": "master",
@@ -1540,7 +1573,7 @@ Example response:
"project_id": 3,
"title": "Et voluptas laudantium minus nihil recusandae ut accusamus earum aut non.",
"description": "Veniam sunt nihil modi earum cumque illum delectus. Nihil ad quis distinctio quia. Autem eligendi at quibusdam repellendus.",
- "state": "opened",
+ "state": "merged",
"created_at": "2016-06-17T07:48:04.330Z",
"updated_at": "2016-07-01T11:14:15.537Z",
"target_branch": "allow_regex_for_project_skip_ref",
diff --git a/doc/api/repositories.md b/doc/api/repositories.md
index 5dbf6cb0760..c8de7f2191d 100644
--- a/doc/api/repositories.md
+++ b/doc/api/repositories.md
@@ -17,6 +17,7 @@ Parameters:
- `path` (optional) - The path inside repository. Used to get content of subdirectories
- `ref` (optional) - The name of a repository branch or tag or if not given the default branch
- `recursive` (optional) - Boolean value used to get a recursive tree (false by default)
+- `per_page` (optional) - Number of results to show per page. If not specified, defaults to `20`
```json
[
diff --git a/doc/api/repository_submodules.md b/doc/api/repository_submodules.md
new file mode 100644
index 00000000000..2e6797f18f4
--- /dev/null
+++ b/doc/api/repository_submodules.md
@@ -0,0 +1,49 @@
+# Repository submodules API
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/41213) in GitLab 11.5
+
+## Update existing submodule reference in repository
+
+In some workflows, especially automated ones, it can be useful to update a
+submodule's reference to keep up to date other projects that use it.
+This endpoint allows you to update a [Git submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules) reference in a
+specific branch.
+
+```
+PUT /projects/:id/repository/submodules/:submodule
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `submodule` | string | yes | URL encoded full path to the submodule. For example, `lib%2Fclass%2Erb` |
+| `branch` | string | yes | Name of the branch to commit into |
+| `commit_sha` | string | yes | Full commit SHA to update the submodule to |
+| `commit_message` | string | no | Commit message. If no message is provided, a default one will be set |
+
+```sh
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repositories/submodules/lib%2Fmodules%2Fexample"
+--data "branch=master&commit_sha=3ddec28ea23acc5caa5d8331a6ecb2a65fc03e88&commit_message=Update submodule reference"
+```
+
+Example response:
+
+```json
+{
+ "id": "ed899a2f4b50b4370feeea94676502b42383c746",
+ "short_id": "ed899a2f4b5",
+ "title": "Updated submodule example_submodule with oid 3ddec28ea23acc5caa5d8331a6ecb2a65fc03e88",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dzaporozhets@sphereconsultinginc.com",
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dzaporozhets@sphereconsultinginc.com",
+ "created_at": "2018-09-20T09:26:24.000-07:00",
+ "message": "Updated submodule example_submodule with oid 3ddec28ea23acc5caa5d8331a6ecb2a65fc03e88",
+ "parent_ids": [
+ "ae1d9fb46aa2b07ee9836d49862ec4e2c46fbbba"
+ ],
+ "committed_date": "2018-09-20T09:26:24.000-07:00",
+ "authored_date": "2018-09-20T09:26:24.000-07:00",
+ "status": null
+}
+```
diff --git a/doc/api/services.md b/doc/api/services.md
index 741ea83070f..f122bac6f1f 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -10,7 +10,7 @@ Asana - Teamwork without email
Set Asana service for a project.
-> This service adds commit messages as comments to Asana tasks. Once enabled, commit messages are checked for Asana task URLs (for example, `https://app.asana.com/0/123456/987654`) or task IDs starting with # (for example, `#987654`). Every task ID found will get the commit comment added to it. You can also close a task with a message containing: `fix #123456`. You can find your Api Keys here: https://asana.com/developers/documentation/getting-started/auth#api-key
+> This service adds commit messages as comments to Asana tasks. Once enabled, commit messages are checked for Asana task URLs (for example, `https://app.asana.com/0/123456/987654`) or task IDs starting with # (for example, `#987654`). Every task ID found will get the commit comment added to it. You can also close a task with a message containing: `fix #123456`. You can find your Api Keys here: <https://asana.com/developers/documentation/getting-started/auth#api-key>.
```
PUT /projects/:id/services/asana
@@ -92,7 +92,7 @@ Parameters:
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `bamboo_url` | string | true | Bamboo root URL like https://bamboo.example.com |
+| `bamboo_url` | string | true | Bamboo root URL. For example, `https://bamboo.example.com`. |
| `build_key` | string | true | Bamboo build plan key like KEY |
| `username` | string | true | A user with API access, if applicable |
| `password` | string | true | Password of the user |
@@ -117,7 +117,7 @@ GET /projects/:id/services/bamboo
Bugzilla Issue Tracker
-### Create/Edit Buildkite service
+### Create/Edit Bugzilla service
Set Bugzilla service for a project.
@@ -168,7 +168,7 @@ Parameters:
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `token` | string | true | Buildkite project GitLab token |
-| `project_url` | string | true | https://buildkite.com/example/project |
+| `project_url` | string | true | `https://buildkite.com/example/project` |
| `enable_ssl_verification` | boolean | false | Enable SSL verification |
### Delete Buildkite service
@@ -278,7 +278,7 @@ Parameters:
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `token` | string | true | Drone CI project specific token |
-| `drone_url` | string | true | http://drone.example.com |
+| `drone_url` | string | true | `http://drone.example.com` |
| `enable_ssl_verification` | boolean | false | Enable SSL verification |
### Delete Drone CI service
@@ -421,7 +421,7 @@ Parameters:
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `webhook` | string | true | The Hangouts Chat webhook. e.g. https://chat.googleapis.com/v1/spaces... |
+| `webhook` | string | true | The Hangouts Chat webhook. For example, `https://chat.googleapis.com/v1/spaces...`. |
| `notify_only_broken_pipelines` | boolean | false | Send notifications for broken pipelines |
| `notify_only_default_branch` | boolean | false | Send notifications only for the default branch |
| `push_events` | boolean | false | Enable notifications for push events |
@@ -470,7 +470,7 @@ Parameters:
| `notify` | boolean | false | Enable notifications |
| `room` | string | false |Room name or ID |
| `api_version` | string | false | Leave blank for default (v2) |
-| `server` | string | false | Leave blank for default. https://hipchat.example.com |
+| `server` | string | false | Leave blank for default. For example, `https://hipchat.example.com`. |
### Delete HipChat service
@@ -496,7 +496,7 @@ Send IRC messages, on update, to a list of recipients through an Irker gateway.
Set Irker (IRC gateway) service for a project.
-> NOTE: Irker does NOT have built-in authentication, which makes it vulnerable to spamming IRC channels if it is hosted outside of a firewall. Please make sure you run the daemon within a secured network to prevent abuse. For more details, read: http://www.catb.org/~esr/irker/security.html.
+> NOTE: Irker does NOT have built-in authentication, which makes it vulnerable to spamming IRC channels if it is hosted outside of a firewall. Please make sure you run the daemon within a secured network to prevent abuse. For more details, read: <http://www.catb.org/~esr/irker/security.html>.
```
PUT /projects/:id/services/irker
@@ -546,7 +546,7 @@ Set JIRA service for a project.
> **Notes:**
> - 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 `project_key`, `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, e.g., `https://jira.example.com`. |
+| `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`. |
| `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. |
@@ -589,7 +589,7 @@ PUT /projects/:id/services/kubernetes
Parameters:
- `namespace` (**required**) - The Kubernetes namespace to use
-- `api_url` (**required**) - The URL to the Kubernetes cluster API, e.g., https://kubernetes.example.com
+- `api_url` (**required**) - The URL to the Kubernetes cluster API. For example, `https://kubernetes.example.com`
- `token` (**required**) - The service token to authenticate against the Kubernetes cluster with
- `ca_pem` (optional) - A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)
@@ -658,7 +658,6 @@ Parameters:
| --------- | ---- | -------- | ----------- |
| `token` | string | yes | The Slack token |
-
### Delete Slack slash command service
Delete Slack slash command service for a project.
@@ -823,7 +822,7 @@ Parameters:
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `api_url` | string | true | Prometheus API Base URL, like http://prometheus.example.com/ |
+| `api_url` | string | true | Prometheus API Base URL. For example, `http://prometheus.example.com/`. |
### Delete Prometheus service
@@ -934,7 +933,7 @@ Parameters:
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `webhook` | string | true | https://hooks.slack.com/services/... |
+| `webhook` | string | true | `https://hooks.slack.com/services/...` |
| `username` | string | false | username |
| `channel` | string | false | Default channel to use if others are not configured |
| `notify_only_broken_pipelines` | boolean | false | Send notifications for broken pipelines |
@@ -988,7 +987,7 @@ Parameters:
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `webhook` | string | true | The Microsoft Teams webhook. e.g. https://outlook.office.com/webhook/... |
+| `webhook` | string | true | The Microsoft Teams webhook. For example, `https://outlook.office.com/webhook/...` |
### Delete Microsoft Teams service
@@ -1024,7 +1023,7 @@ Parameters:
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `webhook` | string | true | The Mattermost webhook. e.g. http://mattermost_host/hooks/... |
+| `webhook` | string | true | The Mattermost webhook. For example, `http://mattermost_host/hooks/...` |
| `username` | string | false | username |
| `channel` | string | false | Default channel to use if others are not configured |
| `notify_only_broken_pipelines` | boolean | false | Send notifications for broken pipelines |
@@ -1080,7 +1079,7 @@ Parameters:
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `teamcity_url` | string | true | TeamCity root URL like https://teamcity.example.com |
+| `teamcity_url` | string | true | TeamCity root URL. For example, `https://teamcity.example.com` |
| `build_type` | string | true | Build configuration ID |
| `username` | string | true | A user with permissions to trigger a manual build |
| `password` | string | true | The password of the user |
@@ -1104,7 +1103,6 @@ GET /projects/:id/services/teamcity
[jira-doc]: ../user/project/integrations/jira.md
[old-jira-api]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/doc/api/services.md#jira
-
## MockCI
Mock an external CI. See [`gitlab-org/gitlab-mock-ci-service`](https://gitlab.com/gitlab-org/gitlab-mock-ci-service) for an example of a companion mock service.
@@ -1123,7 +1121,7 @@ Parameters:
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `mock_service_url` | string | true | http://localhost:4004 |
+| `mock_service_url` | string | true | `http://localhost:4004` |
### Delete MockCI service
diff --git a/doc/ci/autodeploy/quick_start_guide.md b/doc/ci/autodeploy/quick_start_guide.md
index 3836216e951..1473703542d 100644
--- a/doc/ci/autodeploy/quick_start_guide.md
+++ b/doc/ci/autodeploy/quick_start_guide.md
@@ -23,9 +23,9 @@ You need to have the Google Cloud SDK installed. e.g.
On OSX, install [homebrew](https://brew.sh):
1. Install Brew Caskroom: `brew install caskroom/cask/brew-cask`
-2. Install Google Cloud SDK: `brew cask install google-cloud-sdk`
-3. Add `kubectl`: `gcloud components install kubectl`
-4. Log in: `gcloud auth login`
+1. Install Google Cloud SDK: `brew cask install google-cloud-sdk`
+1. Add `kubectl`: `gcloud components install kubectl`
+1. Log in: `gcloud auth login`
Now go back to the Google interface, find your cluster, and follow the instructions under `Connect to the cluster` and open the Kubernetes Dashboard. It will look something like `gcloud container clusters get-credentials ruby-autodeploy \ --zone europe-west2-c --project api-project-XXXXXXX` and then `kubectl proxy`.
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 3b41036cd14..fef367051bf 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -46,18 +46,18 @@ GitLab Runner then executes job scripts as the `gitlab-runner` user.
--description "My Runner"
```
-2. Install Docker Engine on server.
+1. Install Docker Engine on server.
For more information how to install Docker Engine on different systems
checkout the [Supported installations](https://docs.docker.com/engine/installation/).
-3. Add `gitlab-runner` user to `docker` group:
+1. Add `gitlab-runner` user to `docker` group:
```bash
sudo usermod -aG docker gitlab-runner
```
-4. Verify that `gitlab-runner` has access to Docker:
+1. Verify that `gitlab-runner` has access to Docker:
```bash
sudo -u gitlab-runner -H docker info
@@ -75,7 +75,7 @@ GitLab Runner then executes job scripts as the `gitlab-runner` user.
- docker run my-docker-image /script/to/run/tests
```
-5. You can now use `docker` command and install `docker-compose` if needed.
+1. You can now use `docker` command and install `docker-compose` if needed.
NOTE: **Note:**
By adding `gitlab-runner` to the `docker` group you are effectively granting `gitlab-runner` full root permissions.
diff --git a/doc/ci/examples/artifactory_and_gitlab/index.md b/doc/ci/examples/artifactory_and_gitlab/index.md
index 9657f52159e..6aa0edd87b4 100644
--- a/doc/ci/examples/artifactory_and_gitlab/index.md
+++ b/doc/ci/examples/artifactory_and_gitlab/index.md
@@ -16,8 +16,8 @@ to build a [Maven](https://maven.apache.org/) project, deploy it to [Artifactory
You'll create two different projects:
-- `simple-maven-dep`: the app built and deployed to Artifactory (available at https://gitlab.com/gitlab-examples/maven/simple-maven-dep)
-- `simple-maven-app`: the app using the previous one as a dependency (available at https://gitlab.com/gitlab-examples/maven/simple-maven-app)
+- `simple-maven-dep`: the app built and deployed to Artifactory (available at https://gitlab.com/gitlab-examples/maven/simple-maven-dep )
+- `simple-maven-app`: the app using the previous one as a dependency (available at https://gitlab.com/gitlab-examples/maven/simple-maven-app )
We assume that you already have a GitLab account on [GitLab.com](https://gitlab.com/), and that you know the basic usage of Git and [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/).
We also assume that an Artifactory instance is available and reachable from the internet, and that you have valid credentials to deploy on it.
diff --git a/doc/ci/examples/browser_performance.md b/doc/ci/examples/browser_performance.md
index d36e97ebfd3..7c3b3a65675 100644
--- a/doc/ci/examples/browser_performance.md
+++ b/doc/ci/examples/browser_performance.md
@@ -1,14 +1,20 @@
# Browser Performance Testing with the Sitespeed.io container
+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).
+
This example shows how to run the
[Sitespeed.io container](https://hub.docker.com/r/sitespeedio/sitespeed.io/) on
your code by using GitLab CI/CD and [Sitespeed.io](https://www.sitespeed.io)
using Docker-in-Docker.
-First, you need a GitLab Runner with the
+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`, called
-`performance`:
+
+Once you set up the Runner, add a new job to `.gitlab-ci.yml` that
+generates the expected report:
```yaml
performance:
@@ -26,19 +32,22 @@ performance:
- mv sitespeed-results/data/performance.json performance.json
artifacts:
paths:
- - performance.json
- - sitespeed-results/
+ - sitespeed-results/
+ reports:
+ performance: performance.json
```
-The above example will:
+The above example will create a `performance` job in your CI/CD pipeline and will run
+Sitespeed.io against the webpage you defined in `URL` to gather key metrics.
+The [GitLab plugin](https://gitlab.com/gitlab-org/gl-performance) for
+Sitespeed.io is downloaded in order to save the report as a
+[Performance report artifact](https://docs.gitlab.com/ee//ci/yaml/README.html#artifactsreportsperformance)
+that you can later download and analyze.
+Due to implementation limitations we always take the latest Performance artifact available.
-1. Create a `performance` job in your CI/CD pipeline and will run
- Sitespeed.io against the webpage you defined in `URL`.
-1. The [GitLab plugin](https://gitlab.com/gitlab-org/gl-performance) for
- Sitespeed.io is downloaded in order to export key metrics to JSON. The full
- HTML Sitespeed.io report will also be saved as an artifact, and if you have
- [GitLab Pages](../../user/project/pages/index.md) enabled, it can be viewed
- directly in your browser.
+The full HTML Sitespeed.io report will also be saved as an artifact, and if you have
+[GitLab Pages](../../user/project/pages/index.md) enabled, it can be viewed
+directly in your browser.
For further customization options of Sitespeed.io, including the ability to
provide a list of URLs to test, please consult
@@ -46,8 +55,8 @@ 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 about
-[Browser Performance Testing](https://docs.gitlab.com/ee/user/project/merge_requests/browser_performance_testing.html).
+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).
## Performance testing on Review Apps
@@ -106,8 +115,40 @@ performance:
- mv sitespeed-results/data/performance.json performance.json
artifacts:
paths:
- - performance.json
- sitespeed-results/
+ reports:
+ performance: performance.json
```
A complete example can be found in our [Auto DevOps CI YML](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml).
+
+## Previous job definitions
+
+CAUTION: **Caution:**
+Before GitLab 11.5, Performance job and artifact had to be named specifically
+to automatically extract report data and show it in the merge request widget.
+While these old job definitions are still maintained they have been deprecated
+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
+performance:
+ stage: performance
+ image: docker:git
+ variables:
+ URL: https://example.com
+ services:
+ - docker:stable-dind
+ script:
+ - mkdir 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 $URL
+ - mv sitespeed-results/data/performance.json performance.json
+ artifacts:
+ paths:
+ - performance.json
+ - sitespeed-results/
+``` \ No newline at end of file
diff --git a/doc/ci/examples/code_quality.md b/doc/ci/examples/code_quality.md
index 2a7040ecdeb..ae000b9d30d 100644
--- a/doc/ci/examples/code_quality.md
+++ b/doc/ci/examples/code_quality.md
@@ -1,11 +1,18 @@
# Analyze your project's Code Quality
+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).
+
This example shows how to run Code Quality on your code by using GitLab CI/CD
and Docker.
-First, you need GitLab Runner with [docker-in-docker executor][dind].
+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`, called `code_quality`:
+Once you set up the Runner, add a new job to `.gitlab-ci.yml` that
+generates the expected report:
```yaml
code_quality:
@@ -23,27 +30,72 @@ code_quality:
--volume /var/run/docker.sock:/var/run/docker.sock
"registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
artifacts:
- paths: [gl-code-quality-report.json]
+ reports:
+ codequality: gl-code-quality-report.json
```
The above example will create a `code_quality` job in your CI/CD pipeline which
-will scan your source code for code quality issues. The report will be saved
-as an artifact that you can later download and analyze.
+will scan your source code for code quality issues. The report will be saved as a
+[Code Quality report artifact](../../ci/yaml/README.md#artifactsreportscodequality)
+that you can later download and analyze.
+Due to implementation limitations we always take the latest Code Quality artifact available.
TIP: **Tip:**
-Starting with [GitLab Starter][ee] 9.3, this information will
-be automatically extracted and shown right in the merge request widget. To do
-so, the CI/CD job must be named `code_quality` and the artifact path must be
-`gl-code-quality-report.json`.
+For [GitLab Starter][ee] users, this information will be automatically
+extracted and shown right in the merge request widget.
[Learn more on Code Quality in merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html).
+## Previous job definitions
+
CAUTION: **Caution:**
-Code Quality was previously using `codeclimate` and `codequality` for job name and
-`codeclimate.json` for the artifact name. While these old names
-are still maintained they have been deprecated with GitLab 11.0 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.
+Before GitLab 11.5, Code Quality 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
+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:
+ paths: [gl-code-quality-report.json]
+```
+
+Alternatively the job name could be `codeclimate` or `codequality`
+and the artifact name could be `codeclimate.json`.
+These names have been deprecated with GitLab 11.0
+and may be removed in next major release, GitLab 12.0.
+
+For GitLab 10.3 and earlier, the job should look like:
+
+```yaml
+codequality:
+ image: docker:latest
+ variables:
+ DOCKER_DRIVER: overlay
+ services:
+ - docker:dind
+ script:
+ - docker pull codeclimate/codeclimate:0.69.0
+ - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate:0.69.0 init
+ - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate:0.69.0 analyze -f json > codeclimate.json || true
+ artifacts:
+ paths: [codeclimate.json]
+```
[cli]: https://github.com/codeclimate/codeclimate
-[dind]: ../docker/using_docker_build.md#use-docker-in-docker-executor
[ee]: https://about.gitlab.com/pricing/
diff --git a/doc/ci/examples/container_scanning.md b/doc/ci/examples/container_scanning.md
index bc948dc6ea9..68330261910 100644
--- a/doc/ci/examples/container_scanning.md
+++ b/doc/ci/examples/container_scanning.md
@@ -1,13 +1,20 @@
# Container Scanning with GitLab CI/CD
+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.
-All you need is a GitLab Runner with the Docker executor (the shared Runners on
-GitLab.com will work fine). You can then add a new job to `.gitlab-ci.yml`,
-called `container_scanning`:
+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:
@@ -36,32 +43,26 @@ container_scanning:
- 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]
+ 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 an artifact that you
-can later download and analyze.
+for possible vulnerabilities. The report will be saved as a
+[Container Scanning report artifact](https://docs.gitlab.com/ee//ci/yaml/README.html#artifactsreportscontainer_scanning)
+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:**
-Starting with [GitLab Ultimate][ee] 10.4, this information will
-be automatically extracted and shown right in the merge request widget. To do
-so, the CI/CD job must be named `container_scanning` and the artifact path must be
-`gl-container-scanning-report.json`.
-[Learn more on container scanning results shown in merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/container_scanning.html).
-
-CAUTION: **Caution:**
-Before GitLab 11.0, Container Scanning was previously using `sast:container` for job name and
-`gl-sast-container-report.json` for the artifact name. While these old names
-are still maintained, they have been deprecated with GitLab 11.0 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 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`.
@@ -69,4 +70,50 @@ While the old name `sast_container` is still maintained, it has been deprecated
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-variables-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.1
+ - 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/
diff --git a/doc/ci/examples/dast.md b/doc/ci/examples/dast.md
index ff20f0b3b5e..0ca89eb6700 100644
--- a/doc/ci/examples/dast.md
+++ b/doc/ci/examples/dast.md
@@ -1,16 +1,26 @@
# Dynamic Application Security Testing with GitLab CI/CD
+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
-All you need is a GitLab Runner with the Docker executor (the shared Runners on
-GitLab.com will work fine). You can then add a new job to `.gitlab-ci.yml`,
-called `dast`:
+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:
@@ -23,13 +33,16 @@ dast:
- /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]
+ 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 finally write the results in the `gl-dast-report.json` file. You can
-then download and analyze the report artifact in JSON format.
+own) and scan it for possible vulnerabilities. The report will be saved as a
+[DAST report artifact](https://docs.gitlab.com/ee//ci/yaml/README.html#artifactsreportsdast)
+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:
@@ -39,25 +52,51 @@ dast:
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 "john.doe@example.com"
- --auth-password "john-doe-password" || true
+ --auth-username $username
+ --auth-password $password || true
- cp /zap/wrk/gl-dast-report.json .
artifacts:
- paths: [gl-dast-report.json]
+ 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:**
-Starting with [GitLab Ultimate][ee] 10.4, this information will
-be automatically extracted and shown right in the merge request widget. To do
-so, the CI job must be named `dast` and the artifact path must be
-`gl-dast-report.json`.
-[Learn more about DAST results shown in merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/dast.html).
+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/
diff --git a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/cloud_foundry_secret_variables.png b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/cloud_foundry_secret_variables.png
deleted file mode 100644
index 5b5d91ec07a..00000000000
--- a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/cloud_foundry_secret_variables.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/cloud_foundry_variables.png b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/cloud_foundry_variables.png
new file mode 100644
index 00000000000..28323e2d8de
--- /dev/null
+++ b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/cloud_foundry_variables.png
Binary files differ
diff --git a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md
index b88761be56b..3ea81be1569 100644
--- a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md
+++ b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md
@@ -106,10 +106,10 @@ Now, since the steps defined in `.gitlab-ci.yml` require credentials to login
to CF, you'll need to add your CF credentials as [environment
variables](../../variables/README.md#predefined-variables-environment-variables)
on GitLab CI/CD. To set the environment variables, navigate to your project's
-**Settings > CI/CD** and expand **Secret Variables**. Name the variables
+**Settings > CI/CD** and expand **Variables**. Name the variables
`CF_USERNAME` and `CF_PASSWORD` and set them to the correct values.
-![Secret Variable Settings in GitLab](img/cloud_foundry_secret_variables.png)
+![Variable Settings in GitLab](img/cloud_foundry_variables.png)
Once set up, GitLab CI/CD will deploy your app to CF at every push to your
repository's deafult branch. To see the build logs or watch your builds running
diff --git a/doc/ci/examples/deployment/README.md b/doc/ci/examples/deployment/README.md
index f53f7c50281..46effb76d71 100644
--- a/doc/ci/examples/deployment/README.md
+++ b/doc/ci/examples/deployment/README.md
@@ -101,12 +101,12 @@ production:
We created two deploy jobs that are executed on different events:
1. `staging` is executed for all commits that were pushed to `master` branch,
-2. `production` is executed for all pushed tags.
+1. `production` is executed for all pushed tags.
We also use two secure variables:
1. `HEROKU_STAGING_API_KEY` - Heroku API key used to deploy staging app,
-2. `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app.
+1. `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app.
## Storing API keys
@@ -120,7 +120,7 @@ is hidden in the job log.
You access added variable by prefixing it's name with `$` (on non-Windows runners)
or `%` (for Windows Batch runners):
-1. `$SECRET_VARIABLE` - use it for non-Windows runners
-2. `%SECRET_VARIABLE%` - use it for Windows Batch runners
+1. `$VARIABLE` - use it for non-Windows runners
+1. `%VARIABLE%` - use it for Windows Batch runners
Read more about the [CI variables](../../variables/README.md).
diff --git a/doc/ci/examples/deployment/composer-npm-deploy.md b/doc/ci/examples/deployment/composer-npm-deploy.md
index bed379b0254..36358515b84 100644
--- a/doc/ci/examples/deployment/composer-npm-deploy.md
+++ b/doc/ci/examples/deployment/composer-npm-deploy.md
@@ -33,9 +33,9 @@ before_script:
In this particular case, the `npm deploy` script is a Gulp script that does the following:
1. Compile CSS & JS
-2. Create sprites
-3. Copy various assets (images, fonts) around
-4. Replace some strings
+1. Create sprites
+1. Copy various assets (images, fonts) around
+1. Replace some strings
All these operations will put all files into a `build` folder, which is ready to be deployed to a live server.
@@ -43,7 +43,7 @@ All these operations will put all files into a `build` folder, which is ready to
You have multiple options: rsync, scp, sftp and so on. For now, we will use scp.
-To make this work, you need to add a GitLab Secret Variable (accessible on _gitlab.example/your-project-name/variables_). That variable will be called `STAGING_PRIVATE_KEY` and it's the **private** ssh key of your server.
+To make this work, you need to add a GitLab CI/CD Variable (accessible on _gitlab.example/your-project-name/variables_). That variable will be called `STAGING_PRIVATE_KEY` and it's the **private** ssh key of your server.
### Security tip
@@ -62,10 +62,10 @@ before_script:
In order, this means that:
-1. We check if the `ssh-agent` is available and we install it if it's not;
-2. We create the `~/.ssh` folder;
-3. We make sure we're running bash;
-4. We disable host checking (we don't ask for user accept when we first connect to a server; and since every job will equal a first connect, we kind of need this)
+1. We check if the `ssh-agent` is available and we install it if it's not.
+1. We create the `~/.ssh` folder.
+1. We make sure we're running bash.
+1. We disable host checking (we don't ask for user accept when we first connect to a server and since every job will equal a first connect, we kind of need this).
And this is basically all you need in the `before_script` section.
@@ -91,11 +91,11 @@ stage_deploy:
Here's the breakdown:
1. `only:dev` means that this build will run only when something is pushed to the `dev` branch. You can remove this block completely and have everything be ran on every push (but probably this is something you don't want)
-2. `ssh-add ...` we will add that private key you added on the web UI to the docker container
-3. We will connect via `ssh` and create a new `_tmp` folder
-4. We will connect via `scp` and upload the `build` folder (which was generated by a `npm` script) to our previously created `_tmp` folder
-5. We will connect again to `ssh` and move the `live` folder to an `_old` folder, then move `_tmp` to `live`.
-6. We connect to ssh and remove the `_old` folder
+1. `ssh-add ...` we will add that private key you added on the web UI to the docker container
+1. We will connect via `ssh` and create a new `_tmp` folder
+1. We will connect via `scp` and upload the `build` folder (which was generated by a `npm` script) to our previously created `_tmp` folder
+1. We will connect again to `ssh` and move the `live` folder to an `_old` folder, then move `_tmp` to `live`.
+1. We connect to ssh and remove the `_old` folder
What's the deal with the artifacts? We just tell GitLab CI to keep the `build` directory (later on, you can download that as needed).
diff --git a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md
index b75ed87bc91..cae051daa56 100644
--- a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md
+++ b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md
@@ -418,7 +418,7 @@ fully understand [IAM Best Practices in AWS](http://docs.aws.amazon.com/IAM/late
1. Click the **Access Keys** section and **Create New Access Key**. Create the key and keep the id and secret around, you'll need them later
![AWS Access Key Config](img/aws_config_window.png)
1. Go to your GitLab project, click **Settings > CI/CD** on the left sidebar
-1. Expand the **Secret Variables** section
+1. Expand the **Variables** section
![GitLab Secret Config](img/gitlab_config.png)
1. Add a key named `AWS_KEY_ID` and copy the key id from Step 2 into the **Value** textbox
1. Add a key named `AWS_KEY_SECRET` and copy the key secret from Step 2 into the **Value** textbox
diff --git a/doc/ci/examples/laravel_with_gitlab_and_envoy/img/secret_variables_page.png b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/secret_variables_page.png
deleted file mode 100644
index b7906d49dcb..00000000000
--- a/doc/ci/examples/laravel_with_gitlab_and_envoy/img/secret_variables_page.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/examples/laravel_with_gitlab_and_envoy/img/variables_page.png b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/variables_page.png
new file mode 100644
index 00000000000..80d8eb0f4fc
--- /dev/null
+++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/variables_page.png
Binary files differ
diff --git a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
index 70020d461d9..b6989d229d1 100644
--- a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
+++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
@@ -120,12 +120,12 @@ Now, let's add it to your GitLab project as a [variable](../../variables/README.
Variables are user-defined variables and are stored out of `.gitlab-ci.yml`, for security purposes.
They can be added per project by navigating to the project's **Settings** > **CI/CD**.
-![variables page](img/secret_variables_page.png)
-
To the field **KEY**, add the name `SSH_PRIVATE_KEY`, and to the **VALUE** field, paste the private key you've copied earlier.
We'll use this variable in the `.gitlab-ci.yml` later, to easily connect to our remote server as the deployer user without entering its password.
-We also need to add the public key to **Project** > **Settings** > **Repository** as [Deploy Keys](../../../ssh/README.md#deploy-keys), which gives us the ability to access our repository from the server through [SSH protocol](../../../gitlab-basics/command-line-commands.md#start-working-on-your-project).
+![variables page](img/variables_page.png)
+
+We also need to add the public key to **Project** > **Settings** > **Repository** as a [Deploy Key](../../../ssh/README.md#deploy-keys), which gives us the ability to access our repository from the server through [SSH protocol](../../../gitlab-basics/command-line-commands.md#start-working-on-your-project).
```bash
@@ -135,10 +135,10 @@ We also need to add the public key to **Project** > **Settings** > **Repository*
cat ~/.ssh/id_rsa.pub
```
-![deploy keys page](img/deploy_keys_page.png)
-
To the field **Title**, add any name you want, and paste the public key into the **Key** field.
+![deploy keys page](img/deploy_keys_page.png)
+
Now, let's clone our repository on the server just to make sure the `deployer` user has access to the repository.
```bash
diff --git a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
index 087b317ab73..ec0b5aaed09 100644
--- a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
+++ b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
@@ -40,15 +40,17 @@ production:
```
This project has three jobs:
-1. `test` - used to test Django application,
-2. `staging` - used to automatically deploy staging environment every push to `master` branch
-3. `production` - used to automatically deploy production environment for every created tag
+
+- `test` - used to test Django application,
+- `staging` - used to automatically deploy staging environment every push to `master` branch
+- `production` - used to automatically deploy production environment for every created tag
## Store API keys
You'll need to create two variables in `Settings > CI/CD > Variables` on your GitLab project settings:
-1. `HEROKU_STAGING_API_KEY` - Heroku API key used to deploy staging app,
-2. `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app.
+
+- `HEROKU_STAGING_API_KEY` - Heroku API key used to deploy staging app.
+- `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app.
Find your Heroku API key in [Manage Account](https://dashboard.heroku.com/account).
diff --git a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
index 7f9ab1f3a5e..33a353f17f5 100644
--- a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
+++ b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
@@ -36,16 +36,17 @@ production:
```
This project has three jobs:
-1. `test` - used to test Rails application,
-2. `staging` - used to automatically deploy staging environment every push to `master` branch
-3. `production` - used to automatically deploy production environment for every created tag
+
+- `test` - used to test Rails application.
+- `staging` - used to automatically deploy staging environment every push to `master` branch.
+- `production` - used to automatically deploy production environment for every created tag.
## Store API keys
You'll need to create two variables in your project's **Settings > CI/CD > Variables**:
-1. `HEROKU_STAGING_API_KEY` - Heroku API key used to deploy staging app,
-2. `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app.
+- `HEROKU_STAGING_API_KEY` - Heroku API key used to deploy staging app.
+- `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app.
Find your Heroku API key in [Manage Account](https://dashboard.heroku.com/account).
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index 636117536a2..bdc593493ea 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -168,7 +168,7 @@ can be found at <https://docs.gitlab.com/runner/>.
In order to have a functional Runner you need to follow two steps:
1. [Install it][runner-install]
-2. [Configure it](../runners/README.md#registering-a-specific-runner)
+1. [Configure it](../runners/README.md#registering-a-specific-runner)
Follow the links above to set up your own Runner or use a Shared Runner as
described in the next section.
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index 2a179bfbbf0..9c9ea651678 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -138,9 +138,9 @@ project without requiring your authorization, so use it with caution.
An admin can enable/disable a specific Runner for projects:
1. Navigate to **Admin > Runners**
-2. Find the Runner you wish to enable/disable
-3. Click edit on the Runner
-4. Click **Enable** or **Disable** on the project
+1. Find the Runner you wish to enable/disable
+1. Click edit on the Runner
+1. Click **Enable** or **Disable** on the project
## Protected Runners
diff --git a/doc/ci/services/mysql.md b/doc/ci/services/mysql.md
index 338368dbbc9..b76f9618fc9 100644
--- a/doc/ci/services/mysql.md
+++ b/doc/ci/services/mysql.md
@@ -31,7 +31,7 @@ Database: el_duderino
```
If you are wondering why we used `mysql` for the `Host`, read more at
-[How is service linked to the job](../docker/using_docker_images.md#how-is-service-linked-to-the-job).
+[How services are linked to the job](../docker/using_docker_images.md#how-services-are-linked-to-the-job).
You can also use any other docker image available on [Docker Hub][hub-mysql].
For example, to use MySQL 5.5 the service becomes `mysql:5.5`.
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 2d23bf6d2fd..bdbcf8c9435 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -65,6 +65,8 @@ future GitLab releases.**
| **CI_JOB_NAME** | 9.0 | 0.5 | The name of the job as defined in `.gitlab-ci.yml` |
| **CI_JOB_STAGE** | 9.0 | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` |
| **CI_JOB_TOKEN** | 9.0 | 1.2 | Token used for authenticating with the [GitLab Container Registry][registry] and downloading [dependent repositories][dependent-repositories] |
+| **CI_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_JOB_URL** | 11.1 | 0.5 | Job details URL |
| **CI_REPOSITORY_URL** | 9.0 | all | The URL to clone the Git repository |
| **CI_RUNNER_DESCRIPTION** | 8.10 | 0.5 | The description of the runner as saved in GitLab |
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 981aa101dd3..aab5f268ef9 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -74,7 +74,8 @@ A job is defined by a list of parameters that define the job behavior.
| after_script | no | Override a set of commands that are executed after job |
| environment | no | Defines a name of environment to which deployment is done by this job |
| coverage | no | Define code coverage settings for a given job |
-| retry | no | Define how many times a job can be auto-retried in case of a failure |
+| retry | no | Define when and how many times a job can be auto-retried in case of a failure |
+| parallel | no | Defines how many instances of a job should be run in parallel |
### `extends`
@@ -102,7 +103,7 @@ rspec:
- $RSPEC
```
-In the example above, the `rspec` job inherits from the `.tests` template job.
+In the example above, the `rspec` job inherits from the `.tests` template job.
GitLab will perform a reverse deep merge based on the keys. GitLab will:
- Merge the `rspec` contents into `.tests` recursively.
@@ -811,7 +812,7 @@ deploy to production:
> defined, GitLab will automatically trigger a stop action when the associated
> branch is deleted.
-Closing (stoping) environments can be achieved with the `on_stop` keyword defined under
+Closing (stopping) environments can be achieved with the `on_stop` keyword defined under
`environment`. It declares a different job that runs in order to close
the environment.
@@ -1336,6 +1337,81 @@ concatenated into a single file. Use a filename pattern (`junit: rspec-*.xml`),
an array of filenames (`junit: [rspec-1.xml, rspec-2.xml, rspec-3.xml]`), or a
combination thereof (`junit: [rspec.xml, test-results/TEST-*.xml]`).
+#### `artifacts:reports:codequality` **[STARTER]**
+
+> Introduced in GitLab 11.5. Requires GitLab Runner 11.5 and above.
+
+The `codequality` report collects [CodeQuality issues](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html)
+as artifacts.
+
+The collected Code Quality report will be uploaded to GitLab as an artifact and will
+be automatically shown in merge requests.
+
+#### `artifacts:reports:sast` **[ULTIMATE]**
+
+> Introduced in GitLab 11.5. Requires GitLab Runner 11.5 and above.
+
+The `sast` report collects [SAST vulnerabilities](https://docs.gitlab.com/ee/user/project/merge_requests/sast.html)
+as artifacts.
+
+The collected SAST report will be uploaded to GitLab as an artifact and will
+be automatically shown in merge requests, pipeline view and provide data for security
+dashboards.
+
+#### `artifacts:reports:dependency_scanning` **[ULTIMATE]**
+
+> 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)
+as artifacts.
+
+The collected Dependency Scanning report will be uploaded to GitLab as an artifact and will
+be automatically shown in merge requests, pipeline view and provide data for security
+dashboards.
+
+#### `artifacts:reports:container_scanning` **[ULTIMATE]**
+
+> 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)
+as artifacts.
+
+The collected Container Scanning report will be uploaded to GitLab as an artifact and will
+be automatically shown in merge requests, pipeline view and provide data for security
+dashboards.
+
+#### `artifacts:reports:dast` **[ULTIMATE]**
+
+> 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)
+as artifacts.
+
+The collected DAST report will be uploaded to GitLab as an artifact and will
+be automatically shown in merge requests, pipeline view and provide data for security
+dashboards.
+
+#### `artifacts:reports:license_management` **[ULTIMATE]**
+
+> Introduced in GitLab 11.5. Requires GitLab Runner 11.5 and above.
+
+The `license_management` report collects [Licenses](https://docs.gitlab.com/ee/user/project/merge_requests/license_management.html)
+as artifacts.
+
+The collected License Management report will be uploaded to GitLab as an artifact and will
+be automatically shown in merge requests, pipeline view and provide data for security
+dashboards.
+
+#### `artifacts:reports:performance` **[PREMIUM]**
+
+> Introduced in GitLab 11.5. Requires GitLab Runner 11.5 and above.
+
+The `performance` report collects [Performance metrics](https://docs.gitlab.com/ee//user/project/merge_requests/browser_performance_testing.html)
+as artifacts.
+
+The collected Performance 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.
@@ -1432,18 +1508,20 @@ job1:
## `retry`
> [Introduced][ce-12909] in GitLab 9.5.
+> [Behaviour expanded](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21758)
+> in GitLab 11.5 to control on which failures to retry.
`retry` allows you to configure how many times a job is going to be retried in
case of a failure.
-When a job fails, and has `retry` configured it is going to be processed again
+When a job fails and has `retry` configured, it is going to be processed again
up to the amount of times specified by the `retry` keyword.
If `retry` is set to 2, and a job succeeds in a second run (first retry), it won't be retried
again. `retry` value has to be a positive integer, equal or larger than 0, but
lower or equal to 2 (two retries maximum, three runs in total).
-A simple example:
+A simple example to retry in all failure cases:
```yaml
test:
@@ -1451,6 +1529,77 @@ test:
retry: 2
```
+By default, a job will be retried on all failure cases. To have a better control
+on which failures to retry, `retry` can be a hash with with the following keys:
+
+- `max`: The maximum number of retries.
+- `when`: The failure cases to retry.
+
+To retry only runner system failures at maximum two times:
+
+```yaml
+test:
+ script: rspec
+ retry:
+ max: 2
+ when: runner_system_failure
+```
+
+If there is another failure, other than a runner system failure, the job will
+not be retried.
+
+To retry on multiple failure cases, `when` can also be an array of failures:
+
+```yaml
+test:
+ script: rspec
+ retry:
+ max: 2
+ when:
+ - runner_system_failure
+ - stuck_or_timeout_failure
+```
+
+Possible values for `when` are:
+
+<!--
+ Please make sure to update `RETRY_WHEN_IN_DOCUMENTATION` array in
+ `spec/lib/gitlab/ci/config/entry/retry_spec.rb` if you change any of
+ the documented values below. The test there makes sure that all documented
+ values are really valid as a config option and therefore should always
+ stay in sync with this documentation.
+ -->
+
+- `always`: Retry on any failure (default).
+- `unknown_failure`: Retry when the failure reason is unknown.
+- `script_failure`: Retry when the script failed.
+- `api_failure`: Retry on API failure.
+- `stuck_or_timeout_failure`: Retry when the job got stuck or timed out.
+- `runner_system_failure`: Retry if there was a runner system failure (e.g. setting up the job failed).
+- `missing_dependency_failure`: Retry if a dependency was missing.
+- `runner_unsupported`: Retry if the runner was unsupported.
+
+
+## `parallel`
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22631) in GitLab 11.5.
+
+`parallel` allows you to configure how many instances of a job to run in
+parallel. This value has to be greater than or equal to two (2) and less or equal than 50.
+
+This creates N instances of the same job that run in parallel. They're named
+sequentially from `job_name 1/N` to `job_name N/N`.
+
+For every job, `CI_NODE_INDEX` and `CI_NODE_TOTAL` [environment variables](../variables/README.html#predefined-variables-environment-variables) are set.
+
+A simple example:
+
+```yaml
+test:
+ script: rspec
+ parallel: 5
+```
+
## `include`
> Introduced in [GitLab Edition Premium][ee] 10.5.
@@ -2034,4 +2183,4 @@ CI with various languages.
[schedules]: ../../user/project/pipelines/schedules.md
[variables-expressions]: ../variables/README.md#variables-expressions
[ee]: https://about.gitlab.com/gitlab-ee/
-[gitlab-versions]: https://about.gitlab.com/products/ \ No newline at end of file
+[gitlab-versions]: https://about.gitlab.com/products/
diff --git a/doc/development/README.md b/doc/development/README.md
index 14dfe8eb1f3..d2dd62ecac5 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -30,6 +30,7 @@ description: 'Learn how to contribute to GitLab.'
## Backend guides
- [GitLab utilities](utilities.md)
+- [Logging](logging.md)
- [API styleguide](api_styleguide.md) Use this styleguide if you are
contributing to the API.
- [GraphQL API styleguide](api_graphql_styleguide.md) Use this
@@ -51,6 +52,7 @@ description: 'Learn how to contribute to GitLab.'
- [Prometheus metrics](prometheus_metrics.md)
- [Guidelines for reusing abstractions](reusing_abstractions.md)
- [DeclarativePolicy framework](policies.md)
+- [Switching to Rails 5](switching_to_rails5.md)
## Performance guides
diff --git a/doc/development/adding_database_indexes.md b/doc/development/adding_database_indexes.md
index ea6f14da3b9..d1d2b8c4907 100644
--- a/doc/development/adding_database_indexes.md
+++ b/doc/development/adding_database_indexes.md
@@ -28,9 +28,9 @@ to filter data by. Instead one should ask themselves the following questions:
1. Can I write my query in such a way that it re-uses as many existing indexes
as possible?
-2. Is the data going to be large enough that using an index will actually be
+1. Is the data going to be large enough that using an index will actually be
faster than just iterating over the rows in the table?
-3. Is the overhead of maintaining the index worth the reduction in query
+1. Is the overhead of maintaining the index worth the reduction in query
timings?
We'll explore every question in detail below.
@@ -62,7 +62,7 @@ In short:
1. Try to write your query in such a way that it re-uses as many existing
indexes as possible.
-2. Run the query using `EXPLAIN ANALYZE` and study the output to find the most
+1. Run the query using `EXPLAIN ANALYZE` and study the output to find the most
ideal query.
## Data Size
diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md
index 6c6e198a7c3..95722c027ba 100644
--- a/doc/development/api_graphql_styleguide.md
+++ b/doc/development/api_graphql_styleguide.md
@@ -379,7 +379,7 @@ let(:mutation) do
)
end
-it 'returns a successfull response' do
+it 'returns a successful response' do
post_graphql_mutation(mutation, current_user: user)
expect(response).to have_gitlab_http_status(:success)
diff --git a/doc/development/build_test_package.md b/doc/development/build_test_package.md
index 439d228baef..c5f6adfeaeb 100644
--- a/doc/development/build_test_package.md
+++ b/doc/development/build_test_package.md
@@ -4,12 +4,13 @@ While developing a new feature or modifying an existing one, it is helpful if an
installable package (or a docker image) containing those changes is available
for testing. For this very purpose, a manual job is provided in the GitLab CI/CD
pipeline that can be used to trigger a pipeline in the omnibus-gitlab repository
-that will create
-1. A deb package for Ubuntu 16.04, available as a build artifact, and
-2. A docker image, which is pushed to [Omnibus GitLab's container
-registry](https://gitlab.com/gitlab-org/omnibus-gitlab/container_registry)
-(images titled `gitlab-ce` and `gitlab-ee` respectively and image tag is the
-commit which triggered the pipeline).
+that will create:
+
+- A deb package for Ubuntu 16.04, available as a build artifact, and
+- A docker image, which is pushed to [Omnibus GitLab's container
+ registry](https://gitlab.com/gitlab-org/omnibus-gitlab/container_registry)
+ (images titled `gitlab-ce` and `gitlab-ee` respectively and image tag is the
+ commit which triggered the pipeline).
When you push a commit to either the gitlab-ce or gitlab-ee project, the
pipeline for that commit will have a `build-package` manual action you can
diff --git a/doc/development/chaos_endpoints.md b/doc/development/chaos_endpoints.md
new file mode 100644
index 00000000000..403a5b21827
--- /dev/null
+++ b/doc/development/chaos_endpoints.md
@@ -0,0 +1,117 @@
+# Generating chaos in a test GitLab instance
+
+As [Werner Vogels](https://twitter.com/Werner), the CTO at Amazon Web Services, famously put it, **Everything fails, all the time**.
+
+As a developer, it's as important to consider the failure modes in which your software will operate as much as normal operation. Doing so can mean the difference between a minor hiccup leading to a scattering of `500` errors experienced by a tiny fraction of users and a full site outage that affects all users for an extended period.
+
+To paraphrase [Tolstoy](https://en.wikipedia.org/wiki/Anna_Karenina_principle), _all happy servers are alike, but all failing servers are failing in their own way_. Luckily, there are ways we can attempt to simulate these failure modes, and the chaos endpoints are tools for assisting in this process.
+
+Currently, there are four endpoints for simulating the following conditions:
+
+- Slow requests.
+- CPU-bound requests.
+- Memory leaks.
+- Unexpected process crashes.
+
+## Enabling chaos endpoints
+
+For obvious reasons, these endpoints are not enabled by default. They can be enabled by setting the `GITLAB_ENABLE_CHAOS_ENDPOINTS` environment variable to `1`.
+
+For example, if you're using the [GDK](https://gitlab.com/gitlab-org/gitlab-development-kit) this can be done with the following command:
+
+```bash
+GITLAB_ENABLE_CHAOS_ENDPOINTS=1 gdk run
+```
+
+## Securing the chaos endpoints
+
+DANGER: **Danger:**
+It is highly recommended that you secure access to the chaos endpoints using a secret token. This is recommended when enabling these endpoints locally and essential when running in a staging or other shared environment. You should not enable them in production unless you absolutely know what you're doing.
+
+A secret token can be set through the `GITLAB_CHAOS_SECRET` environment variable. For example, when using the [GDK](https://gitlab.com/gitlab-org/gitlab-development-kit) this can be done with the following command:
+
+```bash
+GITLAB_ENABLE_CHAOS_ENDPOINTS=1 GITLAB_CHAOS_SECRET=secret gdk run
+```
+
+Replace `secret` with your own secret token.
+
+## Invoking chaos
+
+Once you have enabled the chaos endpoints and restarted the application, you can start testing using the endpoints.
+
+## Memory leaks
+
+To simulate a memory leak in your application, use the `/-/chaos/leakmem` endpoint.
+
+NOTE: **Note:**
+The memory is not retained after the request finishes. Once the request has completed, the Ruby garbage collector will attempt to recover the memory.
+
+```
+GET /-/chaos/leakmem
+GET /-/chaos/leakmem?memory_mb=1024
+GET /-/chaos/leakmem?memory_mb=1024&duration_s=50
+```
+
+| Attribute | Type | Required | Description |
+| ------------ | ------- | -------- | ---------------------------------------------------------------------------------- |
+| `memory_mb` | integer | no | How much memory, in MB, should be leaked. Defaults to 100MB. |
+| `duration_s` | integer | no | Minimum duration, in seconds, that the memory should be retained. Defaults to 30s. |
+
+```bash
+curl http://localhost:3000/-/chaos/leakmem?memory_mb=1024&duration_s=10 --header 'X-Chaos-Secret: secret'
+```
+
+## CPU spin
+
+This endpoint attempts to fully utilise a single core, at 100%, for the given period.
+
+Depending on your rack server setup, your request may timeout after a predermined period (normally 60 seconds).
+If you're using Unicorn, this is done by killing the worker process.
+
+```
+GET /-/chaos/cpuspin
+GET /-/chaos/cpuspin?duration_s=50
+```
+
+| Attribute | Type | Required | Description |
+| ------------ | ------- | -------- | --------------------------------------------------------------------- |
+| `duration_s` | integer | no | Duration, in seconds, that the core will be utilised. Defaults to 30s |
+
+```bash
+curl http://localhost:3000/-/chaos/cpuspin?duration_s=60 --header 'X-Chaos-Secret: secret'
+```
+
+## Sleep
+
+This endpoint is similar to the CPU Spin endpoint but simulates off-processor activity, such as network calls to backend services. It will sleep for a given duration.
+
+As with the CPU Spin endpoint, this may lead to your request timing out if duration exceeds the configured limit.
+
+```
+GET /-/chaos/sleep
+GET /-/chaos/sleep?duration_s=50
+```
+
+| Attribute | Type | Required | Description |
+| ------------ | ------- | -------- | ---------------------------------------------------------------------- |
+| `duration_s` | integer | no | Duration, in seconds, that the request will sleep for. Defaults to 30s |
+
+```bash
+curl http://localhost:3000/-/chaos/sleep?duration_s=60 --header 'X-Chaos-Secret: secret'
+```
+
+## Kill
+
+This endpoint will simulate the unexpected death of a worker process using a `kill` signal.
+
+NOTE: **Note:**
+Since this endpoint uses the `KILL` signal, the worker is not given a chance to cleanup or shutdown.
+
+```
+GET /-/chaos/kill
+```
+
+```bash
+curl http://localhost:3000/-/chaos/kill --header 'X-Chaos-Secret: secret'
+```
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index 3fe79943fdc..52710e54e86 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -9,11 +9,11 @@ code is effective, understandable, and maintainable.
## Getting your merge request reviewed, approved, and merged
-You are strongly encouraged to get your code **reviewed** by a
+You are strongly encouraged to get your code **reviewed** by a
[reviewer](https://about.gitlab.com/handbook/engineering/#reviewer) as soon as
there is any code to review, to get a second opinion on the chosen solution and
implementation, and an extra pair of eyes looking for bugs, logic problems, or
-uncovered edge cases. The reviewer can be from a different team, but it is
+uncovered edge cases. The reviewer can be from a different team, but it is
recommended to pick someone who knows the domain well. You can read more about the
importance of involving reviewer(s) in the section on the responsibility of the author below.
@@ -23,6 +23,9 @@ one of the [Merge request coaches][team].
Depending on the areas your merge request touches, it must be **approved** by one
or more [maintainers](https://about.gitlab.com/handbook/engineering/#maintainer):
+For approvals, we use the approval functionality found in the merge request
+widget. Reviewers can add their approval by [approving additionally](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html#adding-or-removing-an-approval).
+
1. If your merge request includes backend changes [^1], it must be
**approved by a [backend maintainer](https://about.gitlab.com/handbook/engineering/projects/#gitlab-ce_maintainers_backend)**.
1. If your merge request includes frontend changes [^1], it must be
@@ -39,43 +42,43 @@ or more [maintainers](https://about.gitlab.com/handbook/engineering/#maintainer)
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.
-As described in the section on the responsibility of the maintainer below, you
-are recommended to get your merge request approved and merged by maintainer(s)
+As described in the section on the responsibility of the maintainer below, you
+are recommended to get your merge request approved and merged by maintainer(s)
from other teams than your own.
### The responsibility of the merge request author
The responsibility to find the best solution and implement it lies with the
-merge request author.
+merge request author.
-Before assigning a merge request to a maintainer for approval and merge, they
-should be confident that it actually solves the problem it was meant to solve,
-that it does so in the most appropriate way, that it satisfies all requirements,
-and that there are no remaining bugs, logical problems, or uncovered edge cases.
-The merge request should also have a completed task list in its description and
+Before assigning a merge request to a maintainer for approval and merge, they
+should be confident that it actually solves the problem it was meant to solve,
+that it does so in the most appropriate way, that it satisfies all requirements,
+and that there are no remaining bugs, logical problems, or uncovered edge cases.
+The merge request should also have a completed task list in its description and
a passing CI pipeline to avoid unnecessary back and forth.
To reach the required level of confidence in their solution, an author is expected
-to involve other people in the investigation and implementation processes as
+to involve other people in the investigation and implementation processes as
appropriate.
-They are encouraged to reach out to domain experts to discuss different solutions
-or get an implementation reviewed, to product managers and UX designers to clear
-up confusion or verify that the end result matches what they had in mind, to
+They are encouraged to reach out to domain experts to discuss different solutions
+or get an implementation reviewed, to product managers and UX designers to clear
+up confusion or verify that the end result matches what they had in mind, to
database specialists to get input on the data model or specific queries, or to
any other developer to get an in-depth review of the solution.
If an author is unsure if a merge request needs a domain expert's opinion, that's
-usually a pretty good sign that it does, since without it the required level of
+usually a pretty good sign that it does, since without it the required level of
confidence in their solution will not have been reached.
### The responsibility of the maintainer
Maintainers are responsible for the overall health, quality, and consistency of
-the GitLab codebase, across domains and product areas.
+the GitLab codebase, across domains and product areas.
-Consequently, their reviews will focus primarily on things like overall
-architecture, code organization, separation of concerns, tests, DRYness,
+Consequently, their reviews will focus primarily on things like overall
+architecture, code organization, separation of concerns, tests, DRYness,
consistency, and readability.
Since a maintainer's job only depends on their knowledge of the overall GitLab
@@ -84,12 +87,12 @@ merge requests from any team and in any product area.
In fact, authors are recommended to get their merge requests merged by maintainers
from other teams than their own, to ensure that all code across GitLab is consistent
-and can be easily understood by all contributors, from both inside and outside the
+and can be easily understood by all contributors, from both inside and outside the
company, without requiring team-specific expertise.
Maintainers will do their best to also review the specifics of the chosen solution
before merging, but as they are not necessarily domain experts, they may be poorly
-placed to do so without an unreasonable investment of time. In those cases, they
+placed to do so without an unreasonable investment of time. In those cases, they
will defer to the judgment of the author and earlier reviewers and involved domain
experts, in favor of focusing on their primary responsibilities.
@@ -97,6 +100,9 @@ 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.
+Maintainers should check before merging if the merge request is approved by the
+required approvers.
+
## Best practices
### Everyone
@@ -224,41 +230,41 @@ Enterprise Edition instance. This has some implications:
1. **Query changes** should be tested to ensure that they don't result in worse
performance at the scale of GitLab.com:
1. Generating large quantities of data locally can help.
- 2. Asking for query plans from GitLab.com is the most reliable way to validate
+ 1. Asking for query plans from GitLab.com is the most reliable way to validate
these.
-2. **Database migrations** must be:
+1. **Database migrations** must be:
1. Reversible.
- 2. Performant at the scale of GitLab.com - ask a maintainer to test the
+ 1. Performant at the scale of GitLab.com - ask a maintainer to test the
migration on the staging environment if you aren't sure.
- 3. Categorised correctly:
+ 1. Categorised correctly:
- Regular migrations run before the new code is running on the instance.
- [Post-deployment migrations](post_deployment_migrations.md) run _after_
the new code is deployed, when the instance is configured to do that.
- [Background migrations](background_migrations.md) run in Sidekiq, and
should only be done for migrations that would take an extreme amount of
time at GitLab.com scale.
-3. **Sidekiq workers**
+1. **Sidekiq workers**
[cannot change in a backwards-incompatible way](sidekiq_style_guide.md#removing-or-renaming-queues):
1. Sidekiq queues are not drained before a deploy happens, so there will be
workers in the queue from the previous version of GitLab.
- 2. If you need to change a method signature, try to do so across two releases,
+ 1. If you need to change a method signature, try to do so across two releases,
and accept both the old and new arguments in the first of those.
- 3. Similarly, if you need to remove a worker, stop it from being scheduled in
+ 1. Similarly, if you need to remove a worker, stop it from being scheduled in
one release, then remove it in the next. This will allow existing jobs to
execute.
- 4. Don't forget, not every instance will upgrade to every intermediate version
+ 1. Don't forget, not every instance will upgrade to every intermediate version
(some people may go from X.1.0 to X.10.0, or even try bigger upgrades!), so
try to be liberal in accepting the old format if it is cheap to do so.
-4. **Cached values** may persist across releases. If you are changing the type a
+1. **Cached values** may persist across releases. If you are changing the type a
cached value returns (say, from a string or nil to an array), change the
cache key at the same time.
-5. **Settings** should be added as a
+1. **Settings** should be added as a
[last resort](https://about.gitlab.com/handbook/product/#convention-over-configuration).
If you're adding a new setting in `gitlab.yml`:
1. Try to avoid that, and add to `ApplicationSetting` instead.
- 2. Ensure that it is also
+ 1. Ensure that it is also
[added to Omnibus](https://docs.gitlab.com/omnibus/settings/gitlab.yml.html#adding-a-new-setting-to-gitlab-yml).
-6. **Filesystem access** can be slow, so try to avoid
+1. **Filesystem access** can be slow, so try to avoid
[shared files](shared_files.md) when an alternative solution is available.
### Credits
diff --git a/doc/development/contributing/issue_workflow.md b/doc/development/contributing/issue_workflow.md
index 4661d11b29e..233dc83f95b 100644
--- a/doc/development/contributing/issue_workflow.md
+++ b/doc/development/contributing/issue_workflow.md
@@ -8,7 +8,7 @@ Most issues will have labels for at least one of the following:
- Type: ~"feature proposal", ~bug, ~customer, etc.
- Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc.
-- Team: ~"CI/CD", ~Plan, ~Manage, ~Quality, etc.
+- Team: ~Plan, ~Manage, ~Quality, etc.
- Stage: ~"devops:plan", ~"devops:create", etc.
- Release Scoping: ~Deliverable, ~Stretch, ~"Next Patch Release"
- Priority: ~P1, ~P2, ~P3, ~P4
@@ -61,7 +61,6 @@ people.
The current team labels are:
- ~Configure
-- ~"CI/CD"
- ~Create
- ~Distribution
- ~Documentation
@@ -74,6 +73,7 @@ The current team labels are:
- ~Release
- ~Secure
- ~UX
+- ~Verify
The descriptions on the [labels page][labels-page] explain what falls under the
responsibility of each team.
diff --git a/doc/development/contributing/merge_request_workflow.md b/doc/development/contributing/merge_request_workflow.md
index 1764e2d8b21..5b32b5cd46f 100644
--- a/doc/development/contributing/merge_request_workflow.md
+++ b/doc/development/contributing/merge_request_workflow.md
@@ -171,6 +171,7 @@ the feature you contribute through all of these steps.
1. Added to [the website](https://gitlab.com/gitlab-com/www-gitlab-com/), if relevant
1. Community questions answered
1. Answers to questions radiated (in docs/wiki/support etc.)
+1. [Black-box tests/end-to-end tests](../testing_guide/testing_levels.md#black-box-tests-or-end-to-end-tests) added if required. Please contact [the quality team](https://about.gitlab.com/handbook/engineering/quality/#teams) with any questions
If you add a dependency in GitLab (such as an operating system package) please
consider updating the following and note the applicability of each in your
@@ -185,7 +186,7 @@ merge request:
1. Omnibus package creator https://gitlab.com/gitlab-org/omnibus-gitlab
[definition-of-done]: http://guide.agilealliance.org/guide/definition-of-done.html
-[testing]: ../testing_guide/index.md
+[testing]: ../testing_guide/index.md
---
diff --git a/doc/development/diffs.md b/doc/development/diffs.md
index 4adae5dabe2..43fc125c21d 100644
--- a/doc/development/diffs.md
+++ b/doc/development/diffs.md
@@ -17,20 +17,20 @@ The diffs fetching process _limits_ single file diff sizes and the overall size
then persisted on `merge_request_diff_files` table.
Even though diffs larger than 10% of the value of `ApplicationSettings#diff_max_patch_bytes` are collapsed,
-we still keep them on Postgres. However, diff files larger than defined _safety limits_
+we still keep them on Postgres. However, diff files larger than defined _safety limits_
(see the [Diff limits section](#diff-limits)) are _not_ persisted in the database.
In order to present diffs information on the Merge Request diffs page, we:
1. Fetch all diff files from database `merge_request_diff_files`
-2. Fetch the _old_ and _new_ file blobs in batch to:
- 1. Highlight old and new file content
- 2. Know which viewer it should use for each file (text, image, deleted, etc)
- 3. Know if the file content changed
- 4. Know if it was stored externally
- 5. Know if it had storage errors
-3. If the diff file is cacheable (text-based), it's cached on Redis
-using `Gitlab::Diff::FileCollection::MergeRequestDiff`
+1. Fetch the _old_ and _new_ file blobs in batch to:
+ - Highlight old and new file content
+ - Know which viewer it should use for each file (text, image, deleted, etc)
+ - Know if the file content changed
+ - Know if it was stored externally
+ - Know if it had storage errors
+1. If the diff file is cacheable (text-based), it's cached on Redis
+ using `Gitlab::Diff::FileCollection::MergeRequestDiff`
### Note diffs
@@ -39,9 +39,9 @@ on `NoteDiffFile` (which is associated with the actual `DiffNote`). So instead
of hitting the repository every time we need the diff of the file, we:
1. Check whether we have the `NoteDiffFile#diff` persisted and use it
-2. Otherwise, if it's a current MR revision, use the persisted
-`MergeRequestDiffFile#diff`
-3. In the last scenario, go the the repository and fetch the diff
+1. Otherwise, if it's a current MR revision, use the persisted
+ `MergeRequestDiffFile#diff`
+1. In the last scenario, go the repository and fetch the diff
## Diff limits
diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md
index 51f5ddfc1e0..b8b86ac1bf5 100644
--- a/doc/development/documentation/index.md
+++ b/doc/development/documentation/index.md
@@ -4,9 +4,9 @@ description: Learn how to contribute to GitLab Documentation.
# GitLab Documentation guidelines
- - **General Documentation**: written by the [developers responsible by creating features](#contributing-to-docs). Should be submitted in the same merge request containing code. Feature proposals (by GitLab contributors) should also be accompanied by its respective documentation. They can be later improved by PMs and Technical Writers.
- - **[Technical Articles](#technical-articles)**: written by any [GitLab Team](https://about.gitlab.com/team/) member, GitLab contributors, or [Community Writers](https://about.gitlab.com/handbook/product/technical-writing/community-writers/).
- - **Indexes per topic**: initially prepared by the Technical Writing Team, and kept up-to-date by developers and PMs in the same merge request containing code. They gather all resources for that topic in a single page (user and admin documentation, articles, and third-party docs).
+- **General Documentation**: written by the [developers responsible by creating features](#contributing-to-docs). Should be submitted in the same merge request containing code. Feature proposals (by GitLab contributors) should also be accompanied by its respective documentation. They can be later improved by PMs and Technical Writers.
+- **[Technical Articles](#technical-articles)**: written by any [GitLab Team](https://about.gitlab.com/team/) member, GitLab contributors, or [Community Writers](https://about.gitlab.com/handbook/product/technical-writing/community-writers/).
+- **Indexes per topic**: initially prepared by the Technical Writing Team, and kept up-to-date by developers and PMs in the same merge request containing code. They gather all resources for that topic in a single page (user and admin documentation, articles, and third-party docs).
## Contributing to docs
@@ -41,9 +41,11 @@ how to structure GitLab docs.
## Markdown and styles
-Currently GitLab docs use Redcarpet as [markdown](../../user/markdown.md) engine, but there's an [open discussion](https://gitlab.com/gitlab-com/gitlab-docs/issues/50) for implementing Kramdown in the near future.
+[GitLab docs](https://gitlab.com/gitlab-com/gitlab-docs) uses [GitLab Kramdown](https://gitlab.com/gitlab-org/gitlab_kramdown)
+as markdown engine. Check the [GitLab Markdown Kramdown Guide](https://about.gitlab.com/handbook/product/technical-writing/markdown-guide/)
+for a complete Kramdown reference.
-All the docs follow the [documentation style guidelines](styleguide.md). See [Linting](#linting) for help to follow the guidelines.
+Follow the [documentation style guidelines](styleguide.md) strictly.
## Documentation directory structure
@@ -61,11 +63,11 @@ in small iterations. Please don't create new docs in these folders.
### Documentation files
- 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
+ Do not use another file name and **do not** create `README.md` files.
- **Do not** use special chars and spaces, or capital letters in file names,
-directory names, branch names, and anything that generates a path.
-- Max screenshot size: 100KB
-- We do not support videos (yet)
+ directory names, branch names, and anything that generates a path.
+- Max screenshot size: 100KB.
+- We do not support videos (yet).
### Location and naming documents
@@ -83,16 +85,16 @@ and cross-link between any related content.
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. Any styleguides 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) |
+| 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. Any styleguides 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) |
---
@@ -134,13 +136,13 @@ merge request.
Changing a document's location is not to be taken lightly. Remember that the
documentation is available to all installations under `help/` and not only to
-GitLab.com or http://docs.gitlab.com. Make sure this is discussed with the
+GitLab.com or <http://docs.gitlab.com>. Make sure this is discussed with the
Documentation team beforehand.
If you indeed need to change a document's location, do NOT remove the old
document, but rather replace all of its contents with a new line:
-```
+```md
This document was moved to [another location](path/to/new_doc.md).
```
@@ -154,7 +156,7 @@ For example, if you were to move `doc/workflow/lfs/lfs_administration.md` to
1. Copy `doc/workflow/lfs/lfs_administration.md` to `doc/administration/lfs.md`
1. Replace the contents of `doc/workflow/lfs/lfs_administration.md` with:
- ```
+ ```md
This document was moved to [another location](../../administration/lfs.md).
```
@@ -162,7 +164,7 @@ For example, if you were to move `doc/workflow/lfs/lfs_administration.md` to
A quick way to find them is to use `git grep`. First go to the root directory
where you cloned the `gitlab-ce` repository and then do:
- ```
+ ```sh
git grep -n "workflow/lfs/lfs_administration"
git grep -n "lfs/lfs_administration"
```
@@ -198,6 +200,11 @@ redirect_to: '../path/to/file/README.md'
It supports both full and relative URLs, e.g. `https://docs.gitlab.com/ee/path/to/file.html`, `../path/to/file.html`, `path/to/file.md`. Note that any `*.md` paths will be compiled to `*.html`.
+NOTE: **Note:**
+This redirection method will not provide a redirect fallback on GitLab `/help`. When using
+it, make sure to add a link to the new page on the doc, otherwise it's a dead end for users that
+land on the doc via `/help`.
+
### Redirections for pages with Disqus comments
If the documentation page being relocated already has any Disqus comments,
@@ -223,135 +230,14 @@ redirect_from: 'https://docs.gitlab.com/my-old-location/README.html'
Note: it is necessary to include the file name in the `redirect_from` URL,
even if it's `index.html` or `README.html`.
-## Linting
-
-To help adhere to the [documentation style guidelines](styleguide.md), and to improve the content
- added to documentation, consider locally installing and running documentation linters. This will
- help you catch common issues before raising merge requests for review of documentation.
-
-The following are some suggested linters you can install locally and sample configuration:
-
-- `proselint`
-- `markdownlint`
-
-NOTE: **Note:**
-This list does not limit what other linters you can add to your local documentation writing
- toolchain.
-
-### `proselint`
-
-`proselint` checks for common problems with English prose. It provides a
- [plethora of checks](http://proselint.com/checks/) that are helpful for technical writing.
-
-`proselint` can be used [on the command line](http://proselint.com/utility/), either on a single
- Markdown file or on all Markdown files in a project. For example, to run `proselint` on all
- documentation in the [`gitlab-ce` project](https://gitlab.com/gitlab-org/gitlab-ce), run the
- following commands from within the `gitlab-ce` project:
-
-```sh
-cd doc
-proselint **/*.md
-```
-
-`proselint` can also be run from within editors using plugins. For example, the following plugins
- are available:
-
-- [Sublime Text](https://packagecontrol.io/packages/SublimeLinter-contrib-proselint)
-- [Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=PatrykPeszko.vscode-proselint)
-- [Others](https://github.com/amperser/proselint#plugins-for-other-software)
-
-#### Sample `proselint` configuration
-
-All of the checks are good to use. However, excluding the `typography.symbols` checks might reduce
- noise. The following sample `proselint` configuration disables the `typography.symbols` checks:
-
-```json
-{
- "checks": {
- "typography.symbols": false
- }
-}
-```
-
-A file with `proselint` configuration must be placed in a
- [valid location](https://github.com/amperser/proselint#checks). For example, `~/.config/proselint/config`.
-
-### `markdownlint`
-
-`markdownlint` checks that certain rules ([example](https://github.com/DavidAnson/markdownlint/blob/master/README.md#rules--aliases))
- are followed for Markdown syntax. Our [style guidelines](styleguide.md) elaborate on which choices
- must be made when selecting Markdown syntax for GitLab documentation and this tool helps
- catch deviations from those guidelines.
-
-`markdownlint` can be used [on the command line](https://github.com/igorshubovych/markdownlint-cli#markdownlint-cli--),
- either on a single Markdown file or on all Markdown files in a project. For example, to run
- `markdownlint` on all documentation in the [`gitlab-ce` project](https://gitlab.com/gitlab-org/gitlab-ce),
- run the following commands from within the `gitlab-ce` project:
-
-```sh
-cd doc
-markdownlint **/*.md
-```
-
-`markdownlint` can also be run from within editors using plugins. For example, the following plugins
- are available:
-
-- [Sublime Text](https://packagecontrol.io/packages/SublimeLinter-contrib-markdownlint)
-- [Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint)
-- [Others](https://github.com/DavidAnson/markdownlint#related)
-
-#### Sample `markdownlint` configuration
-
-The following sample `markdownlint` configuration modifies the available default rules to:
-
-- Adhere to the [style guidelines](styleguide.md).
-- Apply conventions found in the GitLab documentation.
-
-```json
-{
- "default": true,
- "header-style": { "style": "atx" },
- "ul-style": { "style": "dash" },
- "line-length": false,
- "no-trailing-punctuation": false,
- "ol-prefix": { "style": "one" },
- "blanks-around-fences": false,
- "hr-style": { "style": "---" },
- "fenced-code-language": false
-}
-```
-
-For [`markdownlint`](https://github.com/DavidAnson/markdownlint/), this configuration must be
- placed in a [valid location](https://github.com/igorshubovych/markdownlint-cli#configuration). For
- example, `~/.markdownlintrc`.
-
-## Testing
-
-We treat documentation as code, thus have implemented some testing.
-Currently, the following tests are in place:
-
-1. `docs lint`: Check that all internal (relative) links work correctly and
- that all cURL examples in API docs use the full switches. It's recommended
- to [check locally](#previewing-locally) before pushing to GitLab by executing the command
- `bundle exec nanoc check internal_links` on your local
- [`gitlab-docs`](https://gitlab.com/gitlab-com/gitlab-docs) directory.
-1. [`ee_compat_check`](../automatic_ce_ee_merge.md#avoiding-ce-gt-ee-merge-conflicts-beforehand) (runs on CE only):
- When you submit a merge request to GitLab Community Edition (CE),
- there is this additional job that runs against Enterprise Edition (EE)
- and checks if your changes can apply cleanly to the EE codebase.
- If that job fails, read the instructions in the job log for what to do next.
- As CE is merged into EE once a day, it's important to avoid merge conflicts.
- Submitting an EE-equivalent merge request cherry-picking all commits from CE to EE is
- essential to avoid them.
-
## Branch naming
If your contribution contains **only** documentation changes, you can speed up
the CI process by following some branch naming conventions. You have three
choices:
-| Branch name | Valid example |
-| ----------- | ------------- |
+| Branch name | Valid example |
+|:----------------------|:-----------------------------|
| Starting with `docs/` | `docs/update-api-issues` |
| Starting with `docs-` | `docs-update-api-issues` |
| Ending in `-docs` | `123-update-api-issues-docs` |
@@ -359,15 +245,6 @@ choices:
If your branch name matches any of the above, it will run only the docs
tests. If it doesn't, the whole test suite will run (including docs).
-## Danger bot
-
-GitLab uses [danger bot](https://github.com/danger/danger) for some elements in
-code review. For docs changes in merge requests, the following actions are taken:
-
-1. Whenever a change under `/doc` is made, the bot leaves a comment for the
- author to mention `@gl-docsteam`, so that the docs can be properly
- reviewed.
-
## Merge requests for GitLab documentation
Before getting started, make sure you read the introductory section
@@ -400,114 +277,15 @@ Follow this [method for cherry-picking from CE to EE](../automatic_ce_ee_merge.m
- Create the EE-equivalent branch ending with `-ee`, e.g.,
`git checkout -b docs-example-ee`
- Once all the jobs are passing in CE and EE, and you've addressed the
-feedback from your own team, assign the CE MR to a technical writer for review
+ feedback from your own team, assign the CE MR to a technical writer for review
- When both MRs are ready, the EE merge request will be merged first, and the
-CE-equivalent will be merged next.
+ CE-equivalent will be merged next.
- Note that the review will occur only in the CE MR, as the EE MR
-contains the same commits as the CE MR.
+ contains the same commits as the CE MR.
- If you have a few more changes that apply to the EE-version only, you can submit
-a couple more commits to the EE branch, but ask the reviewer to review the EE merge request
-additionally to the CE MR. If there are many EE-only changes though, start a new MR
-to EE only.
-
-## Previewing the changes live
-
-NOTE: **Note:**
-To preview your changes to documentation locally, follow this
-[development guide](https://gitlab.com/gitlab-com/gitlab-docs/blob/master/README.md#development-when-contributing-to-gitlab-documentation) or [these instructions for GDK](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/gitlab_docs.md).
-
-The live preview is currently enabled for the following projects:
-
-- https://gitlab.com/gitlab-org/gitlab-ce
-- https://gitlab.com/gitlab-org/gitlab-ee
-- https://gitlab.com/gitlab-org/gitlab-runner
-
-If your branch contains only documentation changes, you can use
-[special branch names](#branch-naming) to avoid long running pipelines.
-
-For [docs-only changes](#branch-naming), the review app is run automatically.
-For all other branches, you can use the manual `review-docs-deploy-manual` job
-in your merge request. You will need at least Maintainer permissions to be able
-to run it. In the mini pipeline graph, you should see an `>>` icon. Clicking on it will
-reveal the `review-docs-deploy-manual` job. Hit the play button for the job to start.
-
-![Manual trigger a docs build](img/manual_build_docs.png)
-
-NOTE: **Note:**
-You will need to push a branch to those repositories, it doesn't work for forks.
-
-The `review-docs-deploy*` job will:
-
-1. Create a new branch in the [gitlab-docs](https://gitlab.com/gitlab-com/gitlab-docs)
- project named after the scheme: `$DOCS_GITLAB_REPO_SUFFIX-$CI_ENVIRONMENT_SLUG`,
- where `DOCS_GITLAB_REPO_SUFFIX` is the suffix for each product, e.g, `ce` for
- CE, etc.
-1. Trigger a cross project pipeline and build the docs site with your changes
-
-After a few minutes, the Review App will be deployed and you will be able to
-preview the changes. The docs URL can be found in two places:
-
-- In the merge request widget
-- In the output of the `review-docs-deploy*` job, which also includes the
- triggered pipeline so that you can investigate whether something went wrong
-
-TIP: **Tip:**
-Someone that has no merge rights to the CE/EE projects (think of forks from
-contributors) will not be able to run the manual job. In that case, you can
-ask someone from the GitLab team who has the permissions to do that for you.
-
-NOTE: **Note:**
-Make sure that you always delete the branch of the merge request you were
-working on. If you don't, the remote docs branch won't be removed either,
-and the server where the Review Apps are hosted will eventually be out of
-disk space.
-
-### Troubleshooting review apps
-
-In case the review app URL returns 404, follow these steps to debug:
-
-1. **Did you follow the URL from the merge request widget?** If yes, then check if
- the link is the same as the one in the job output.
-1. **Did you follow the URL from the job output?** If yes, then it means that
- either the site is not yet deployed or something went wrong with the remote
- pipeline. Give it a few minutes and it should appear online, otherwise you
- can check the status of the remote pipeline from the link in the job output.
- If the pipeline failed or got stuck, drop a line in the `#docs` chat channel.
-
-### Technical aspects
-
-If you want to know the hot details, here's what's really happening:
-
-1. You manually run the `review-docs-deploy` job in a CE/EE merge request.
-1. The job runs the [`scripts/trigger-build-docs`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/scripts/trigger-build-docs)
- script with the `deploy` flag, which in turn:
- 1. Takes your branch name and applies the following:
- - The slug of the branch name is used to avoid special characters since
- ultimately this will be used by NGINX.
- - The `preview-` prefix is added to avoid conflicts if there's a remote branch
- with the same name that you created in the merge request.
- - The final branch name is truncated to 42 characters to avoid filesystem
- limitations with long branch names (> 63 chars).
- 1. The remote branch is then created if it doesn't exist (meaning you can
- re-run the manual job as many times as you want and this step will be skipped).
- 1. A new cross-project pipeline is triggered in the docs project.
- 1. The preview URL is shown both at the job output and in the merge request
- widget. You also get the link to the remote pipeline.
-1. In the docs project, the pipeline is created and it
- [skips the test jobs](https://gitlab.com/gitlab-com/gitlab-docs/blob/8d5d5c750c602a835614b02f9db42ead1c4b2f5e/.gitlab-ci.yml#L50-55)
- to lower the build time.
-1. Once the docs site is built, the HTML files are uploaded as artifacts.
-1. A specific Runner tied only to the docs project, runs the Review App job
- that downloads the artifacts and uses `rsync` to transfer the files over
- to a location where NGINX serves them.
-
-The following GitLab features are used among others:
-
-- [Manual actions](../../ci/yaml/README.md#manual-actions)
-- [Multi project pipelines](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html)
-- [Review Apps](../../ci/review_apps/index.md)
-- [Artifacts](../../ci/yaml/README.md#artifacts)
-- [Specific Runner](../../ci/runners/README.md#locking-a-specific-runner-from-being-enabled-for-other-projects)
+ a couple more commits to the EE branch, but ask the reviewer to review the EE merge request
+ additionally to the CE MR. If there are many EE-only changes though, start a new MR
+ to EE only.
## GitLab `/help`
@@ -516,8 +294,8 @@ Every GitLab instance includes the documentation, which is available from `/help
The documentation available online on docs.gitlab.com is continuously
deployed every hour from the `master` branch of CE, EE, Omnibus, and Runner. Therefore,
-once a merge request gets merged, it will be available online on the same day,
-but they will be shipped (and available on `/help`) within the milestone assigned
+once a merge request gets merged, it will be available online on the same day.
+However, they will be shipped (and available on `/help`) within the milestone assigned
to the MR.
For instance, let's say your merge request has a milestone set to 11.3, which
@@ -614,7 +392,7 @@ They should be placed in a new directory named `/article-title/index.md` under a
- **User guides**: technical content to guide regular users from point A to point B
- **Admin guides**: technical content to guide administrators of GitLab instances from point A to point B
- **Technical Overviews**: technical content describing features, solutions, and third-party integrations
-- **Tutorials**: technical content provided step-by-step on how to do things, or how to reach very specific objectives
+- **Tutorials**: technical content provided step-by-step on how to do things, or how to reach specific objectives
#### Understanding guides, tutorials, and technical overviews
@@ -646,7 +424,6 @@ with the following information:
For example:
-
```yaml
---
author: John Doe
@@ -661,5 +438,250 @@ date: 2017-02-01
Use the [writing method](https://about.gitlab.com/handbook/product/technical-writing/#writing-method) defined by the Technical Writing team.
+## Previewing the changes live
+
+NOTE: **Note:**
+To preview your changes to documentation locally, follow this
+[development guide](https://gitlab.com/gitlab-com/gitlab-docs/blob/master/README.md#development-when-contributing-to-gitlab-documentation) or [these instructions for GDK](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/gitlab_docs.md).
+
+The live preview is currently enabled for the following projects:
+
+- <https://gitlab.com/gitlab-org/gitlab-ce>
+- <https://gitlab.com/gitlab-org/gitlab-ee>
+- <https://gitlab.com/gitlab-org/gitlab-runner>
+
+If your branch contains only documentation changes, you can use
+[special branch names](#branch-naming) to avoid long running pipelines.
+
+For [docs-only changes](#branch-naming), the review app is run automatically.
+For all other branches, you can use the manual `review-docs-deploy-manual` job
+in your merge request. You will need at least Maintainer permissions to be able
+to run it. In the mini pipeline graph, you should see an `>>` icon. Clicking on it will
+reveal the `review-docs-deploy-manual` job. Hit the play button for the job to start.
+
+![Manual trigger a docs build](img/manual_build_docs.png)
+
+NOTE: **Note:**
+You will need to push a branch to those repositories, it doesn't work for forks.
+
+The `review-docs-deploy*` job will:
+
+1. Create a new branch in the [gitlab-docs](https://gitlab.com/gitlab-com/gitlab-docs)
+ project named after the scheme: `$DOCS_GITLAB_REPO_SUFFIX-$CI_ENVIRONMENT_SLUG`,
+ where `DOCS_GITLAB_REPO_SUFFIX` is the suffix for each product, e.g, `ce` for
+ CE, etc.
+1. Trigger a cross project pipeline and build the docs site with your changes
+
+After a few minutes, the Review App will be deployed and you will be able to
+preview the changes. The docs URL can be found in two places:
+
+- In the merge request widget
+- In the output of the `review-docs-deploy*` job, which also includes the
+ triggered pipeline so that you can investigate whether something went wrong
+
+TIP: **Tip:**
+Someone that has no merge rights to the CE/EE projects (think of forks from
+contributors) will not be able to run the manual job. In that case, you can
+ask someone from the GitLab team who has the permissions to do that for you.
+
+NOTE: **Note:**
+Make sure that you always delete the branch of the merge request you were
+working on. If you don't, the remote docs branch won't be removed either,
+and the server where the Review Apps are hosted will eventually be out of
+disk space.
+
+### Troubleshooting review apps
+
+In case the review app URL returns 404, follow these steps to debug:
+
+1. **Did you follow the URL from the merge request widget?** If yes, then check if
+ the link is the same as the one in the job output.
+1. **Did you follow the URL from the job output?** If yes, then it means that
+ either the site is not yet deployed or something went wrong with the remote
+ pipeline. Give it a few minutes and it should appear online, otherwise you
+ can check the status of the remote pipeline from the link in the job output.
+ If the pipeline failed or got stuck, drop a line in the `#docs` chat channel.
+
+### Technical aspects
+
+If you want to know the in-depth details, here's what's really happening:
+
+1. You manually run the `review-docs-deploy` job in a CE/EE merge request.
+1. The job runs the [`scripts/trigger-build-docs`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/scripts/trigger-build-docs)
+ script with the `deploy` flag, which in turn:
+ 1. Takes your branch name and applies the following:
+ - The slug of the branch name is used to avoid special characters since
+ ultimately this will be used by NGINX.
+ - The `preview-` prefix is added to avoid conflicts if there's a remote branch
+ with the same name that you created in the merge request.
+ - The final branch name is truncated to 42 characters to avoid filesystem
+ limitations with long branch names (> 63 chars).
+ 1. The remote branch is then created if it doesn't exist (meaning you can
+ re-run the manual job as many times as you want and this step will be skipped).
+ 1. A new cross-project pipeline is triggered in the docs project.
+ 1. The preview URL is shown both at the job output and in the merge request
+ widget. You also get the link to the remote pipeline.
+1. In the docs project, the pipeline is created and it
+ [skips the test jobs](https://gitlab.com/gitlab-com/gitlab-docs/blob/8d5d5c750c602a835614b02f9db42ead1c4b2f5e/.gitlab-ci.yml#L50-55)
+ to lower the build time.
+1. Once the docs site is built, the HTML files are uploaded as artifacts.
+1. A specific Runner tied only to the docs project, runs the Review App job
+ that downloads the artifacts and uses `rsync` to transfer the files over
+ to a location where NGINX serves them.
+
+The following GitLab features are used among others:
+
+- [Manual actions](../../ci/yaml/README.md#manual-actions)
+- [Multi project pipelines](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html)
+- [Review Apps](../../ci/review_apps/index.md)
+- [Artifacts](../../ci/yaml/README.md#artifacts)
+- [Specific Runner](../../ci/runners/README.md#locking-a-specific-runner-from-being-enabled-for-other-projects)
+
+## Testing
+
+We treat documentation as code, thus have implemented some testing.
+Currently, the following tests are in place:
+
+1. `docs lint`: Check that all internal (relative) links work correctly and
+ that all cURL examples in API docs use the full switches. It's recommended
+ to [check locally](#previewing-locally) before pushing to GitLab by executing the command
+ `bundle exec nanoc check internal_links` on your local
+ [`gitlab-docs`](https://gitlab.com/gitlab-com/gitlab-docs) directory.
+1. [`ee_compat_check`](../automatic_ce_ee_merge.md#avoiding-ce-gt-ee-merge-conflicts-beforehand) (runs on CE only):
+ When you submit a merge request to GitLab Community Edition (CE),
+ there is this additional job that runs against Enterprise Edition (EE)
+ and checks if your changes can apply cleanly to the EE codebase.
+ If that job fails, read the instructions in the job log for what to do next.
+ As CE is merged into EE once a day, it's important to avoid merge conflicts.
+ Submitting an EE-equivalent merge request cherry-picking all commits from CE to EE is
+ essential to avoid them.
+
+### Linting
+
+To help adhere to the [documentation style guidelines](styleguide.md), and to improve the content
+added to documentation, consider locally installing and running documentation linters. This will
+help you catch common issues before raising merge requests for review of documentation.
+
+The following are some suggested linters you can install locally and sample configuration:
+
+- [`proselint`](#proselint)
+- [`markdownlint`](#markdownlint)
+
+NOTE: **Note:**
+This list does not limit what other linters you can add to your local documentation writing toolchain.
+
+#### `proselint`
+
+`proselint` checks for common problems with English prose. It provides a
+ [plethora of checks](http://proselint.com/checks/) that are helpful for technical writing.
+
+`proselint` can be used [on the command line](http://proselint.com/utility/), either on a single
+ Markdown file or on all Markdown files in a project. For example, to run `proselint` on all
+ documentation in the [`gitlab-ce` project](https://gitlab.com/gitlab-org/gitlab-ce), run the
+ following commands from within the `gitlab-ce` project:
+
+```sh
+cd doc
+proselint **/*.md
+```
+
+`proselint` can also be run from within editors using plugins. For example, the following plugins
+ are available:
+
+- [Sublime Text](https://packagecontrol.io/packages/SublimeLinter-contrib-proselint)
+- [Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=PatrykPeszko.vscode-proselint)
+- [Others](https://github.com/amperser/proselint#plugins-for-other-software)
+
+##### Sample `proselint` configuration
+
+All of the checks are good to use. However, excluding the `typography.symbols` and `misc.phrasal_adjectives` checks will reduce
+noise. The following sample `proselint` configuration disables these checks:
+
+```json
+{
+ "checks": {
+ "typography.symbols": false,
+ "misc.phrasal_adjectives": false
+ }
+}
+```
+
+A file with `proselint` configuration must be placed in a
+[valid location](https://github.com/amperser/proselint#checks). For example, `~/.config/proselint/config`.
+
+#### `markdownlint`
+
+`markdownlint` checks that certain rules ([example](https://github.com/DavidAnson/markdownlint/blob/master/README.md#rules--aliases))
+ are followed for Markdown syntax. Our [style guidelines](styleguide.md) elaborate on which choices
+ must be made when selecting Markdown syntax for GitLab documentation and this tool helps
+ catch deviations from those guidelines.
+
+`markdownlint` can be used [on the command line](https://github.com/igorshubovych/markdownlint-cli#markdownlint-cli--),
+ either on a single Markdown file or on all Markdown files in a project. For example, to run
+ `markdownlint` on all documentation in the [`gitlab-ce` project](https://gitlab.com/gitlab-org/gitlab-ce),
+ run the following commands from within the `gitlab-ce` project:
+
+```sh
+cd doc
+markdownlint **/*.md
+```
+
+`markdownlint` can also be run from within editors using plugins. For example, the following plugins
+ are available:
+
+- [Sublime Text](https://packagecontrol.io/packages/SublimeLinter-contrib-markdownlint)
+- [Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint)
+- [Others](https://github.com/DavidAnson/markdownlint#related)
+
+##### Sample `markdownlint` configuration
+
+The following sample `markdownlint` configuration modifies the available default rules to:
+
+- Adhere to the [style guidelines](styleguide.md).
+- Apply conventions found in the GitLab documentation.
+- Allow the flexibility of using some inline HTML.
+
+```json
+{
+ "default": true,
+ "header-style": { "style": "atx" },
+ "ul-style": { "style": "dash" },
+ "line-length": false,
+ "no-trailing-punctuation": false,
+ "ol-prefix": { "style": "one" },
+ "blanks-around-fences": false,
+ "no-inline-html": {
+ "allowed_elements": [
+ "table",
+ "tbody",
+ "tr",
+ "td",
+ "ul",
+ "ol",
+ "li",
+ "br",
+ "img",
+ "a",
+ "strong",
+ "i",
+ "div"
+ ]
+ },
+ "hr-style": { "style": "---" },
+ "fenced-code-language": false
+}
+```
+
+For [`markdownlint`](https://github.com/DavidAnson/markdownlint/), this configuration must be
+placed in a [valid location](https://github.com/igorshubovych/markdownlint-cli#configuration). For
+example, `~/.markdownlintrc`.
+
+## Danger bot
+
+GitLab uses [danger bot](https://github.com/danger/danger) for some elements in
+code review. For docs changes in merge requests, whenever a change under `/doc`
+is made, the bot leaves a comment for the author to mention `@gl-docsteam`, so
+that the docs can be properly reviewed.
+
[gitlab-map]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png
[graffle]: https://gitlab.com/gitlab-org/gitlab-design/blob/d8d39f4a87b90fb9ae89ca12dc565347b4900d5e/production/resources/gitlab-map.graffle
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index c43f91278de..8309ba9a72c 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -10,17 +10,15 @@ GitLab documentation. Check the
Check the GitLab handbook for the [writing styles guidelines](https://about.gitlab.com/handbook/communication/#writing-style-guidelines).
-For help adhering to the guidelines, see [Linting](index.md#linting).
+For help adhering to the guidelines, see [linting](index.md#linting).
## Files
- [Directory structure](index.md#location-and-naming-documents): place the docs
in the correct location.
- [Documentation files](index.md#documentation-files): name the files accordingly.
-- [Markdown](../../user/markdown.md): use the GitLab Flavored Markdown in the
-documentation.
-NOTE: **Note:**
+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.
@@ -28,65 +26,144 @@ 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.
-## Text
+### Markdown
+
+The [documentation website](https://docs.gitlab.com) had its markdown engine migrated from [Redcarpet to GitLab Kramdown](https://gitlab.com/gitlab-com/gitlab-docs/merge_requests/108)
+in October, 2018.
+
+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 on this style guide. For a complete
+Kramdown reference, check the [GiLab Markdown Kramdown Guide](https://about.gitlab.com/handbook/product/technical-writing/markdown-guide/).
+Use Kramdown markup wisely: do not overuse its specific markup (e.g., `{:.class}`) as it will not render properly in
+[`/help`](#gitlab-help).
+
+## Content
-- Split up long lines (wrap text), this makes it much easier to review and edit. Only
- double line breaks are shown as a full line break in [GitLab markdown][gfm].
- 80-100 characters is a good line length.
- Make sure that the documentation is added in the correct
- [directory](index.md#documentation-directory-structure) and that
- there's a link to it somewhere useful.
+ [directory](index.md#documentation-directory-structure), linked from its
+ higher-level index, and linked from other related pages.
- Do not duplicate information.
- Be brief and clear.
-- Unless there's a logical reason not to, add documents in alphabetical order.
+- Unless there's a logical reason not to, structure the document in alphabetical order
+(headings, tables, and lists).
- Write in US English.
-- Use [single spaces][] instead of double spaces.
-- Jump a line between different markups (e.g., after every paragraph, header, list, etc)
- Capitalize "G" and "L" in GitLab.
-- Use sentence case for titles, headings, labels, menu items, and buttons.
- Use title case when referring to [features](https://about.gitlab.com/features/) or
[products](https://about.gitlab.com/pricing/) (e.g., GitLab Runner, Geo,
Issue Boards, GitLab Core, Git, Prometheus, Kubernetes, etc), and methods or methodologies
(e.g., Continuous Integration, Continuous Deployment, Scrum, Agile, etc). Note that
-some features are also objects (e.g. "Merge Requests" and "merge requests").
+some features are also objects (e.g. "GitLab's Merge Requests support X." and "Create a new merge request for Z.").
-## Formatting
+## Text
+
+- Split up long lines (wrap text), this makes it much easier to review and edit. Only
+ double line breaks are shown as a full line break by creating new paragraphs.
+ 80-100 characters is the recommended line length.
+- Use sentence case for titles, headings, labels, menu items, and buttons.
+- Jump a line between different markups (e.g., after every paragraph, header, list, etc). Example:
-- Use double asterisks (`**`) to mark a word or text in bold (`**bold**`).
-- Use undescore (`_`) for text in italics (`_italic_`).
-- Put an empty line between different markups. For example:
```md
## Header
Paragraph.
- - List item
- - List item
+ - List item 1
+ - List item 2
```
-### Punctuation
+## Emphasis
+
+- Use double asterisks (`**`) to mark a word or text in bold (`**bold**`).
+- Use undescore (`_`) for text in italics (`_italic_`).
+- Use greater than (`>`) for blockquotes.
+
+## Punctuation
+
+Check the general punctuation rules for the GitLab documentation on the table below.
+Check specific punctuation rules for [list items](#list-items) below.
+
+| Rule | Example |
+| ---- | ------- |
+| Always end full sentences with a period. | _For a complete overview, read through this document._|
+| Always add a space after a period when beginning a new sentence | _For a complete overview, check this doc. For other references, check out this guide._ |
+| Do not use double spaces. | --- |
+| Do not use tabs for indentation. Use spaces instead. You can configure your code editor to output spaces instead of tabs when pressing the tab key. | --- |
+| Use serial commas ("Oxford commas") before the final 'and/or' in a list. | _You can create new issues, merge requests, and milestones._ |
+| Always add a space before and after dashes when using it in a sentence (for replacing a comma, for example). | _You should try this - or not._ |
+| Always use lowercase after a colon. | _Related Issues: a way to create a relationship between issues._ |
+
+## List items
+
+- Always start list items with a capital letter.
+- Always leave a blank line before and after a list.
+- Begin a line with spaces (not tabs) to denote a subitem.
+- To nest subitems, indent them with two spaces.
+- To nest code blocks, indent them with four spaces.
+- Only use ordered lists when their items describe a sequence of steps to follow.
-For punctuation rules, please refer to the [GitLab UX guide](https://design.gitlab.com/content/punctuation/).
+**Markup:**
-### Ordered and unordered lists
+- Use dashes (`- `) for unordered lists instead of asterisks (`* `).
+- Use the number one (`1`) for each item in an ordered list.
+When rendered, the list items will appear with sequential numbering.
-- Use dashes (`-`) for unordered lists instead of asterisks (`*`).
-- Use the number one (`1`) for ordered lists.
+**Punctuation:**
+
+- Do not add commas (`,`) or semicolons (`;`) to the end of a list item.
+- Only add periods to the end of a list item if the item consists of a complete sentence. The [definition of full sentence](https://www2.le.ac.uk/offices/ld/resources/writing/grammar/grammar-guides/sentence) is: _"a complete sentence always contains a verb, expresses a complete idea, and makes sense standing alone"_.
+- Be consistent throughout the list: if the majority of the items do not end in a period, do not end any of the items in a period, even if they consist of a complete sentence. The opposite is also valid: if the majority of the items end with a period, end all with a period.
- Separate list items from explanatory text with a colon (`:`). For example:
+
```md
The list is as follows:
- - First item: This explains the first item.
- - Second item: This explains the second item.
+ - First item: this explains the first item.
+ - Second item: this explains the second item.
```
-- For further guidance on punctuation in bullet lists, please refer to the [GitLab UX guide](https://design.gitlab.com/content/punctuation/).
+
+**Examples:**
+
+Do:
+
+- First list item
+- Second list item
+- Third list item
+
+Don't:
+
+- First list item
+- Second list item
+- Third list item.
+
+Do:
+
+- Let's say this is a complete sentence.
+- Let's say this is also a complete sentence.
+- Not a complete sentence.
+
+Don't:
+
+- Let's say this is a complete sentence.
+- Let's say this is also a complete sentence.
+- Not a complete sentence
+
+## Quotes
+
+Valid for markdown content only, not for frontmatter entries:
+
+- Standard quotes: double quotes (`"`). Example: "This is wrapped in double quotes".
+- Quote within a quote: double quotes (`"`) wrap single quotes (`'`). Example: "I am 'quoting' something within a quote".
+
+For other punctuation rules, please refer to the
+[GitLab UX guide](https://design.gitlab.com/content/punctuation/).
## Headings
- Add **only one H1** in each document, by adding `#` at the beginning of
it (when using markdown). The `h1` will be the document `<title>`.
-- Start with an h2 (`##`), and respect the order h2 > h3 > h4 > h5 > h6.
- Never skip the hierarchy level, such as h2 > h4
+- Start with an `h2` (`##`), and respect the order `h2` > `h3` > `h4` > `h5` > `h6`.
+ Never skip the hierarchy level, such as `h2` > `h4`
- Avoid putting numbers in headings. Numbers shift, hence documentation anchor
links shift too, which eventually leads to dead links. If you think it is
compelling to add numbers in headings, make sure to at least discuss it with
@@ -96,21 +173,19 @@ For punctuation rules, please refer to the [GitLab UX guide](https://design.gitl
- Avoid adding things that show ephemeral statuses. For example, if a feature is
considered beta or experimental, put this info in a note, not in the heading.
- When introducing a new document, be careful for the headings to be
- grammatically and syntactically correct. Mention one or all
- of the following GitLab members for a review: `@axil` or `@marcia`.
+ grammatically and syntactically correct. Mention an [assigned technical writer (TW)](https://about.gitlab.com/handbook/product/categories/)
+ for review.
This is to ensure that no document with wrong heading is going
live without an audit, thus preventing dead links and redirection issues when
corrected.
- Leave exactly one new line after a heading.
+- Do not use links in headings.
+- Add the corresponding [product badge](#product-badges) according to the tier the feature belongs.
## Links
-- Use the regular inline link markdown markup `[Text](https://example.com)`.
- It's easier to read, review, and maintain.
-- If there's a link that repeats several times through the same document,
- you can use `[Text][identifier]` and at the bottom of the section or the
- document add: `[identifier]: https://example.com`, in which case, we do
- encourage you to also add an alternative text: `[identifier]: https://example.com "Alternative text"` that appears when hovering your mouse on a link.
+- Use inline link markdown markup `[Text](https://example.com)`.
+ It's easier to read, review, and maintain. **Do not** use `[Text][identifier]`.
- To link to internal documentation, use relative links, not full URLs. Use `../` to
navigate tp high-level directories, and always add the file name `file.md` at the
end of the link with the `.md` extension, not `.html`.
@@ -128,11 +203,12 @@ For punctuation rules, please refer to the [GitLab UX guide](https://design.gitl
To indicate the steps of navigation through the UI:
+
- Use the exact word as shown in the UI, including any capital letters as-is.
-- Use bold text for navigation items and the char `>` as separator
+- Use bold text for navigation items and the char "greater than" (`>`) as separator
(e.g., `Navigate to your project's **Settings > CI/CD**` ).
- If there are any expandable menus, make sure to mention that the user
-needs to expand the tab to find the settings you're referring to.
+needs to expand the tab to find the settings you're referring to (e.g., `Navigate to your project's **Settings > CI/CD** and expand **General pipelines**`).
## Images
@@ -141,7 +217,7 @@ needs to expand the tab to find the settings you're referring to.
names with the name of the document that they will be included in. For
example, if there is a document called `twitter.md`, then a valid image name
could be `twitter_login_screen.png`.
-- Images should have a specific, non-generic name that will differentiate them.
+- Images should have a specific, non-generic name that will differentiate and describe them properly.
- Keep all file names in lower case.
- Consider using PNG images instead of JPEG.
- Compress all images with <https://tinypng.com/> or similar tool.
@@ -164,13 +240,39 @@ Inside the document:
- If a heading is placed right after an image, always add three dashes (`---`)
between the image and the heading.
+## Code blocks
+
+- Always wrap code added to a sentence in inline code blocks (``` ` ```).
+E.g., `.gitlab-ci.yml`, `git add .`, `CODEOWNERS`, `only: master`.
+File names, commands, entries, and anything that refers to code should be added to code blocks.
+To make things easier for the user, always add a full code block for things that can be
+useful to copy and paste, as they can easily do it with the button on code blocks.
+- For regular code blocks, always use a highlighting class corresponding to the
+language for better readability. Examples:
+
+ ```md
+ ```ruby
+ Ruby code
+ ```
+
+ ```js
+ JavaScript code
+ ```
+
+ ```md
+ Markdown code
+ ```
+ ```
+
+- For a complete reference on code blocks, check the [Kramdown guide](https://about.gitlab.com/handbook/product/technical-writing/markdown-guide/#code-blocks).
+
## Alert boxes
Whenever you want to call the attention to a particular sentence,
use the following markup for highlighting.
_Note that the alert boxes only work for one paragraph only. Multiple paragraphs,
-lists, headers, etc will not render correctly._
+lists, headers, etc will not render correctly. For multiple lines, use blockquotes instead._
### Note
@@ -234,6 +336,31 @@ which renders in docs.gitlab.com to:
If the text spans across multiple lines it's OK to split the line.
+For multiple paragraphs, use the symbol `>` before every line:
+
+```md
+> This is the first paragraph.
+>
+> This is the second paragraph.
+>
+> - This is a list item
+> - Second item in the list
+>
+> ### This is an `h3`
+```
+
+Which renders to:
+
+> This is the first paragraph.
+>
+> This is the second paragraph.
+>
+> - This is a list item
+> - Second item in the list
+>
+> ### This is an `h3`
+>{:.no_toc}
+
## Specific sections and terms
To mention and/or reference specific terms in GitLab, please follow the styles
@@ -290,18 +417,18 @@ feature availability:
To exclude GitLab.com tiers (when the feature is not available in GitLab.com), add the
keyword "only":
+- For GitLab Core: `**[CORE ONLY]**`.
- For GitLab Starter: `**[STARTER ONLY]**`.
- For GitLab Premium: `**[PREMIUM ONLY]**`.
- For GitLab Ultimate: `**[ULTIMATE ONLY]**`.
-- For GitLab Core: `**[CORE ONLY]**`.
The tier should be ideally added to headers, so that the full badge will be displayed.
-But 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.
+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.
E.g., `**[STARTER]**` renders **[STARTER]**, `**[STARTER ONLY]**` renders **[STARTER ONLY]**.
The absence of tiers' mentions mean that the feature is available in GitLab Core,
-GitLab.com Free, and higher tiers.
+GitLab.com Free, and all higher tiers.
#### How it works
@@ -317,7 +444,7 @@ avoid duplication, link to the special document that can be found in
[`doc/administration/restart_gitlab.md`][doc-restart]. Usually the text will
read like:
-```
+```md
Save the file and [reconfigure GitLab](../../administration/restart_gitlab.md)
for the changes to take effect.
```
@@ -348,8 +475,8 @@ prefer to document it in the CE docs to avoid duplication.
Configuration settings include:
-- Settings that touch configuration files in `config/`.
-- NGINX settings and settings in `lib/support/` in general.
+1. Settings that touch configuration files in `config/`.
+1. NGINX settings and settings in `lib/support/` in general.
When there is a list of steps to perform, usually that entails editing the
configuration file and reconfiguring/restarting GitLab. In such case, follow
@@ -386,13 +513,13 @@ the style below as a guide:
In this case:
-- before each step list the installation method is declared in bold
-- three dashes (`---`) are used to create a horizontal line and separate the
+- Before each step list the installation method is declared in bold
+- Three dashes (`---`) are used to create a horizontal line and separate the
two methods
-- the code blocks are indented one or more spaces under the list item to render
+- The code blocks are indented one or more spaces under the list item to render
correctly
-- different highlighting languages are used for each config in the code block
-- the [references](#references) guide is used for reconfigure/restart
+- Different highlighting languages are used for each config in the code block
+- The [references](#references) guide is used for reconfigure/restart
### Fake tokens
@@ -403,19 +530,19 @@ low.
You can use the following fake tokens as examples.
-| **Token type** | **Token value** |
-| --------------------- | --------------------------------- |
-| Private user token | `9koXpg98eAheJpvBs5tK` |
-| Personal access token | `n671WNGecHugsdEDPsyo` |
+| **Token type** | **Token value** |
+|:----------------------|:-------------------------------------------------------------------|
+| Private user token | `9koXpg98eAheJpvBs5tK` |
+| Personal access token | `n671WNGecHugsdEDPsyo` |
| Application ID | `2fcb195768c39e9a94cec2c2e32c59c0aad7a3365c10892e8116b5d83d4096b6` |
| Application secret | `04f294d1eaca42b8692017b426d53bbc8fe75f827734f0260710b83a556082df` |
-| Secret CI variable | `Li8j-mLUVA3eZYjPfd_H` |
-| Specific Runner token | `yrnZW46BrtBFqM7xDzE7dddd` |
-| Shared Runner token | `6Vk7ZsosqQyfreAxXTZr` |
-| Trigger token | `be20d8dcc028677c931e04f3871a9b` |
-| Webhook secret token | `6XhDroRcYPM5by_h-HLY` |
-| Health check token | `Tu7BgjR9qeZTEyRzGG2P` |
-| Request profile token | `7VgpS4Ax5utVD2esNstz` |
+| CI/CD variable | `Li8j-mLUVA3eZYjPfd_H` |
+| Specific Runner token | `yrnZW46BrtBFqM7xDzE7dddd` |
+| Shared Runner token | `6Vk7ZsosqQyfreAxXTZr` |
+| Trigger token | `be20d8dcc028677c931e04f3871a9b` |
+| Webhook secret token | `6XhDroRcYPM5by_h-HLY` |
+| Health check token | `Tu7BgjR9qeZTEyRzGG2P` |
+| Request profile token | `7VgpS4Ax5utVD2esNstz` |
### API
@@ -438,16 +565,16 @@ on this document. Further explanation is given below.
Use the following table headers to describe the methods. Attributes should
always be in code blocks using backticks (``` ` ```).
-```
+```md
| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
+|:----------|:-----|:---------|:------------|
```
Rendered example:
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `user` | string | yes | The GitLab username |
+| Attribute | Type | Required | Description |
+|:----------|:-------|:---------|:--------------------|
+| `user` | string | yes | The GitLab username |
#### cURL commands
@@ -459,12 +586,12 @@ Rendered example:
- Prefer to use examples using the personal access token and don't pass data of
username and password.
-| Methods | Description |
-| ------- | ----------- |
+| Methods | Description |
+|:-------------------------------------------|:------------------------------------------------------|
| `-H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK"` | Use this method as is, whenever authentication needed |
-| `-X POST` | Use this method when creating new objects |
-| `-X PUT` | Use this method when updating existing objects |
-| `-X DELETE` | Use this method when removing existing objects |
+| `-X POST` | Use this method when creating new objects |
+| `-X PUT` | Use this method when updating existing objects |
+| `-X DELETE` | Use this method when removing existing objects |
#### cURL Examples
diff --git a/doc/development/documentation/workflow.md b/doc/development/documentation/workflow.md
index 52bc059a925..75ce8640e87 100644
--- a/doc/development/documentation/workflow.md
+++ b/doc/development/documentation/workflow.md
@@ -7,16 +7,16 @@ description: Learn the process of shipping documentation for GitLab.
At GitLab, developers contribute new or updated documentation along with their code, but product managers and technical writers also have essential roles in the process.
- Product Managers (PMs): in the issue for all new and updated features,
-PMs include specific documentation requirements that the developer who is
-writing or updating the docs must meet, along with feature descriptions
-and use cases. They call out any specific areas where collaborating with
-a technical writer is recommended, and usually act as the first reviewer
-of the docs.
+ PMs include specific documentation requirements that the developer who is
+ writing or updating the docs must meet, along with feature descriptions
+ and use cases. They call out any specific areas where collaborating with
+ a technical writer is recommended, and usually act as the first reviewer
+ of the docs.
- Developers: author documentation and merge it on time (up to a week after
-the feature freeze).
+ the feature freeze).
- Technical Writers: review each issue to ensure PM's requirements are complete,
-help developers with any questions throughout the process, and act as the final
-reviewer of all new and updated docs content before it's merged.
+ help developers with any questions throughout the process, and act as the final
+ reviewer of all new and updated docs content before it's merged.
## Requirements
@@ -112,17 +112,17 @@ and the missed-deliverable due date (the 14th of each month) are both respected.
The developer should add to the feature MR the documentation containing:
- The [documentation blurb](structure.md#documentation-blurb): copy the
-feature name, overview/description, and use cases from the feature issue
+ feature name, overview/description, and use cases from the feature issue
- Instructions: write how to use the feature, step by step, with no gaps.
- [Crosslink for discoverability](structure.md#discoverability): link with
-internal docs and external resources (if applicable)
+ internal docs and external resources (if applicable)
- Index: link the new doc or the new heading from the higher-level index
-for [discoverability](#discoverability)
+ for [discoverability](#discoverability)
- [Screenshots](styleguide.md#images): when necessary, add screenshots for:
- Illustrating a step of the process
- Indicating the location of a navigation menu
- Label the MR with `Documentation`, `Deliverable`, `docs-P1`, and assign
-the correct milestone
+ the correct milestone
- Assign the PM for review
- When done, mention the `@gl\-docsteam` in the MR asking for review
- **Due date**: feature freeze date and time
@@ -133,10 +133,10 @@ If the docs aren't being shipped within the feature MR:
- Create a new issue mentioning "docs" or "documentation" in the title (use the Documentation issue description template)
- Label the issue with: `Documentation`, `Deliverable`, `docs-P1`, `<product-label>`
-(product label == CI/CD, Pages, Prometheus, etc)
+ (product label == CI/CD, Pages, Prometheus, etc)
- Add the correct milestone
- Create a new MR for shipping the docs changes and follow the same
-process [described above](#documentation-shipped-in-the-feature-mr)
+ process [described above](#documentation-shipped-in-the-feature-mr)
- Use the MR description template called "Documentation"
- Add the same labels and milestone as you did for the issue
- Assign the PM for review
@@ -162,9 +162,9 @@ The **due date** for **merging** `missed-deliverable` MRs is on the
- **Planning**
- Once an issue contains a Documentation label and the current milestone, a
-technical writer reviews the Product Manager's documentation requirements
+ technical writer reviews the Product Manager's documentation requirements.
- Once the documentation requirements are approved, the technical writer can
-work with the developer to discuss any documentation questions and plans/outlines, as needed.
+ work with the developer to discuss any documentation questions and plans/outlines, as needed.
- **Review** - A technical writer must review the documentation for:
- Clarity
@@ -183,4 +183,3 @@ work with the developer to discuss any documentation questions and plans/outline
- Describe the difference between new features and feature updates
- Creating a new doc vs updating an existing doc
-->
-
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index b6f053ff0e9..9aea03139ee 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -119,10 +119,20 @@ This also applies to views.
### EE features based on CE features
-For features that build on existing CE features, write a module in the
-`EE` namespace and `prepend` it in the CE class. This makes conflicts
-less likely to happen during CE to EE merges because only one line is
-added to the CE class - the `prepend` line.
+For features that build on existing CE features, write a module in the `EE`
+namespace and `prepend` it in the CE class, on the last line of the file that
+the class resides in. This makes conflicts less likely to happen during CE to EE
+merges because only one line is added to the CE class - the `prepend` line. For
+example, to prepend a module into the `User` class you would use the following
+approach:
+
+```ruby
+class User < ActiveRecord::Base
+ # ... lots of code here ...
+end
+
+User.prepend(EE::User)
+```
Since the module would require an `EE` namespace, the file should also be
put in an `ee/` sub-directory. For example, we want to extend the user model
@@ -231,7 +241,6 @@ the existing file:
```ruby
class ApplicationController < ActionController::Base
- prepend EE::ApplicationController
# ...
def after_sign_out_path_for(resource)
@@ -240,6 +249,8 @@ class ApplicationController < ActionController::Base
# ...
end
+
+ApplicationController.prepend(EE::ApplicationController)
```
And create a new file in the `ee/` sub-directory with the altered
@@ -533,8 +544,6 @@ module API
end
end
- prepend EE::API::MergeRequests
-
params :optional_params do
# CE specific params go here...
@@ -542,6 +551,8 @@ module API
end
end
end
+
+API::MergeRequests.prepend(EE::API::MergeRequests)
```
And then we could override it in EE module:
@@ -582,10 +593,10 @@ module API
authorize_read_builds!
end
end
-
- prepend EE::API::JobArtifacts
end
end
+
+API::JobArtifacts.prepend(EE::API::JobArtifacts)
```
And then we can follow regular object-oriented practices to override it:
@@ -626,8 +637,6 @@ module API
end
end
- prepend EE::API::MergeRequests
-
put ':id/merge_requests/:merge_request_iid/merge' do
merge_request = find_project_merge_request(params[:merge_request_iid])
@@ -639,6 +648,8 @@ module API
end
end
end
+
+API::MergeRequests.prepend(EE::API::MergeRequests)
```
Note that `update_merge_request_ee` doesn't do anything in CE, but
@@ -676,27 +687,37 @@ or not we really need to extend it from EE. For now we're not using it much.
Sometimes we need to use different arguments for a particular API route, and we
can't easily extend it with an EE module because Grape has different context in
-different blocks. In order to overcome this, we could use class methods from the
-API class.
+different blocks. In order to overcome this, we need to move the data to a class
+method that resides in a separate module or class. This allows us to extend that
+module or class before its data is used, without having to place a `prepend` in
+the middle of CE code.
For example, in one place we need to pass an extra argument to
`at_least_one_of` so that the API could consider an EE-only argument as the
-least argument. This is not quite beautiful but it's working:
+least argument. We would approach this as follows:
```ruby
+# api/merge_requests/parameters.rb
module API
class MergeRequests < Grape::API
- def self.update_params_at_least_one_of
- %i[
- assignee_id
- description
- ]
+ module Parameters
+ def self.update_params_at_least_one_of
+ %i[
+ assignee_id
+ description
+ ]
+ end
end
+ end
+end
- prepend EE::API::MergeRequests
+API::MergeRequests::Parameters.prepend(EE::API::MergeRequests::Parameters)
+# api/merge_requests.rb
+module API
+ class MergeRequests < Grape::API
params do
- at_least_one_of(*::API::MergeRequests.update_params_at_least_one_of)
+ at_least_one_of(*Parameters.update_params_at_least_one_of)
end
end
end
@@ -708,16 +729,18 @@ And then we could easily extend that argument in the EE class method:
module EE
module API
module MergeRequests
- extend ActiveSupport::Concern
+ module Parameters
+ extend ActiveSupport::Concern
- class_methods do
- extend ::Gitlab::Utils::Override
+ class_methods do
+ extend ::Gitlab::Utils::Override
- override :update_params_at_least_one_of
- def update_params_at_least_one_of
- super.push(*%i[
- squash
- ])
+ override :update_params_at_least_one_of
+ def update_params_at_least_one_of
+ super.push(*%i[
+ squash
+ ])
+ end
end
end
end
@@ -728,6 +751,78 @@ end
It could be annoying if we need this for a lot of routes, but it might be the
simplest solution right now.
+This approach can also be used when models define validations that depend on
+class methods. For example:
+
+```ruby
+# app/models/identity.rb
+class Identity < ActiveRecord::Base
+ def self.uniqueness_scope
+ [:provider]
+ end
+
+ prepend EE::Identity
+
+ validates :extern_uid,
+ allow_blank: true,
+ uniqueness: { scope: uniqueness_scope, case_sensitive: false }
+end
+
+# ee/app/models/ee/identity.rb
+module EE
+ module Identity
+ extend ActiveSupport::Concern
+
+ class_methods do
+ extend ::Gitlab::Utils::Override
+
+ def uniqueness_scope
+ [*super, :saml_provider_id]
+ end
+ end
+ end
+end
+```
+
+Instead of taking this approach, we would refactor our code into the following:
+
+```ruby
+# ee/app/models/ee/identity/uniqueness_scopes.rb
+module EE
+ module Identity
+ module UniquenessScopes
+ extend ActiveSupport::Concern
+
+ class_methods do
+ extend ::Gitlab::Utils::Override
+
+ def uniqueness_scope
+ [*super, :saml_provider_id]
+ end
+ end
+ end
+ end
+end
+
+# app/models/identity/uniqueness_scopes.rb
+class Identity < ActiveRecord::Base
+ module UniquenessScopes
+ def self.uniqueness_scope
+ [:provider]
+ end
+ end
+end
+
+Identity::UniquenessScopes.prepend(EE::Identity::UniquenessScopes)
+
+# app/models/identity.rb
+class Identity < ActiveRecord::Base
+ validates :extern_uid,
+ allow_blank: true,
+ uniqueness: { scope: Identity::UniquenessScopes.scopes, case_sensitive: false }
+end
+```
+
### Code in `spec/`
When you're testing EE-only features, avoid adding examples to the
diff --git a/doc/development/fe_guide/icons.md b/doc/development/fe_guide/icons.md
index 3d8da6accc1..533e2001300 100644
--- a/doc/development/fe_guide/icons.md
+++ b/doc/development/fe_guide/icons.md
@@ -3,7 +3,7 @@
We manage our own Icon and Illustration library in the [gitlab-svgs][gitlab-svgs] repository.
This repository is published on [npm][npm] and managed as a dependency via yarn.
You can browse all available Icons and Illustrations [here][svg-preview].
-To upgrade to a new version run `yarn upgrade @gitlab-org/gitlab-svgs`.
+To upgrade to a new version run `yarn upgrade @gitlab/svgs`.
## Icons
@@ -111,6 +111,6 @@ export default {
</template>
```
-[npm]: https://www.npmjs.com/package/@gitlab-org/gitlab-svgs
+[npm]: https://www.npmjs.com/package/@gitlab/svgs
[gitlab-svgs]: https://gitlab.com/gitlab-org/gitlab-svgs
[svg-preview]: https://gitlab-org.gitlab.io/gitlab-svgs
diff --git a/doc/development/fe_guide/style_guide_scss.md b/doc/development/fe_guide/style_guide_scss.md
index 48eb6d0a7d6..b09243598d5 100644
--- a/doc/development/fe_guide/style_guide_scss.md
+++ b/doc/development/fe_guide/style_guide_scss.md
@@ -183,9 +183,11 @@ Don't use ID selectors in CSS.
```
### Variables
+
Before adding a new variable for a color or a size, guarantee:
-1. There isn't already one
-2. There isn't a similar one we can use instead.
+
+- There isn't already one
+- There isn't a similar one we can use instead.
## Linting
diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md
index f6cbd11042c..ccfd465531a 100644
--- a/doc/development/fe_guide/vue.md
+++ b/doc/development/fe_guide/vue.md
@@ -221,6 +221,14 @@ const vm = mountComponent(Component, data);
The main return value of a Vue component is the rendered output. In order to test the component we
need to test the rendered output. [Vue][vue-test] guide's to unit test show us exactly that:
+## Vue.js Expert Role
+One should apply to be a Vue.js expert by opening an MR when the Merge Request's they create and review show:
+- Deep understanding of Vue and Vuex reactivy
+- Vue and Vuex code are structured according to both official and our guidelines
+- Full understanding of testing a Vue and Vuex application
+- Vuex code follows the [documented pattern](./vuex.md#actions-pattern-request-and-receive-namespaces)
+- Knowledge about the existing Vue and Vuex applications and existing reusable components
+
[vue-docs]: http://vuejs.org/guide/index.html
[issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards
diff --git a/doc/development/fe_guide/vuex.md b/doc/development/fe_guide/vuex.md
index f582f5da323..0f57835fb87 100644
--- a/doc/development/fe_guide/vuex.md
+++ b/doc/development/fe_guide/vuex.md
@@ -114,19 +114,21 @@ When a request is made we often want to show a loading state to the user.
Instead of creating an action to toggle the loading state and dispatch it in the component,
create:
+
1. An action `requestSomething`, to toggle the loading state
1. An action `receiveSomethingSuccess`, to handle the success callback
1. An action `receiveSomethingError`, to handle the error callback
1. An action `fetchSomething` to make the request.
1. In case your application does more than a `GET` request you can use these as examples:
- 1. `PUT`: `createSomething`
- 2. `POST`: `updateSomething`
- 3. `DELETE`: `deleteSomething`
+ - `PUT`: `createSomething`
+ - `POST`: `updateSomething`
+ - `DELETE`: `deleteSomething`
The component MUST only dispatch the `fetchNamespace` action. Actions namespaced with `request` or `receive` should not be called from the component
The `fetch` action will be responsible to dispatch `requestNamespace`, `receiveNamespaceSuccess` and `receiveNamespaceError`
By following this pattern we guarantee:
+
1. All applications follow the same pattern, making it easier for anyone to maintain the code
1. All data in the application follows the same lifecycle pattern
1. Actions are contained and human friendly
@@ -297,12 +299,12 @@ export default {
```javascript
// component.vue
-
+
// bad
created() {
this.$store.commit('mutation');
}
-
+
// good
created() {
this.$store.dispatch('action');
diff --git a/doc/development/github_importer.md b/doc/development/github_importer.md
index 0d558583bb8..e860bde48dc 100644
--- a/doc/development/github_importer.md
+++ b/doc/development/github_importer.md
@@ -99,8 +99,8 @@ This worker will wrap up the import process by performing some housekeeping
Advancing stages is done in one of two ways:
-1. Scheduling the worker for the next stage directly.
-2. Scheduling a job for `Gitlab::GithubImport::AdvanceStageWorker` which will
+- Scheduling the worker for the next stage directly.
+- Scheduling a job for `Gitlab::GithubImport::AdvanceStageWorker` which will
advance the stage when all work of the current stage has been completed.
The first approach should only be used by workers that perform all their work in
@@ -147,7 +147,7 @@ We handle this by doing the following:
1. Once we hit the rate limit all jobs will automatically reschedule themselves
in such a way that they are not executed until the rate limit has been reset.
-2. We cache the mapping of GitHub users to GitLab users in Redis.
+1. We cache the mapping of GitHub users to GitLab users in Redis.
More information on user caching can be found below.
@@ -157,21 +157,21 @@ When mapping GitHub users to GitLab users we need to (in the worst case)
perform:
1. One API call to get the user's Email address.
-2. Two database queries to see if a corresponding GitLab user exists. One query
+1. Two database queries to see if a corresponding GitLab user exists. One query
will try to find the user based on the GitHub user ID, while the second query
is used to find the user using their GitHub Email address.
Because this process is quite expensive we cache the result of these lookups in
Redis. For every user looked up we store three keys:
-1. A Redis key mapping GitHub usernames to their Email addresses.
-2. A Redis key mapping a GitHub Email addresses to a GitLab user ID.
-3. A Redis key mapping a GitHub user ID to GitLab user ID.
+- A Redis key mapping GitHub usernames to their Email addresses.
+- A Redis key mapping a GitHub Email addresses to a GitLab user ID.
+- A Redis key mapping a GitHub user ID to GitLab user ID.
There are two types of lookups we cache:
-1. A positive lookup, meaning we found a GitLab user ID.
-2. A negative lookup, meaning we didn't find a GitLab user ID. Caching this
+- A positive lookup, meaning we found a GitLab user ID.
+- A negative lookup, meaning we didn't find a GitLab user ID. Caching this
prevents us from performing the same work for users that we know don't exist
in our GitLab database.
diff --git a/doc/development/i18n/proofreader.md b/doc/development/i18n/proofreader.md
index f58d79fccf1..c4ac53f45ac 100644
--- a/doc/development/i18n/proofreader.md
+++ b/doc/development/i18n/proofreader.md
@@ -41,6 +41,7 @@ are very appreciative of the work done by translators and proofreaders!
- Nikita Grylov - [GitLab](https://gitlab.com/nixel2007), [Crowdin](https://crowdin.com/profile/nixel2007)
- Alexy Lustin - [GitLab](https://gitlab.com/allustin), [Crowdin](https://crowdin.com/profile/lustin)
- Spanish
+ - Pedro Garcia - [GitLab](https://gitlab.com/pedgarrod), [Crowdin](https://crowdin.com/profile/breaking_pitt)
- Ukrainian
- Volodymyr Sobotovych - [GitLab](https://gitlab.com/wheleph), [Crowdin](https://crowdin.com/profile/wheleph)
- Andrew Vityuk - [GitLab](https://gitlab.com/3_1_3_u), [Crowdin](https://crowdin.com/profile/andruwa13)
diff --git a/doc/development/instrumentation.md b/doc/development/instrumentation.md
index 7761f65d78a..bef166f2aec 100644
--- a/doc/development/instrumentation.md
+++ b/doc/development/instrumentation.md
@@ -117,11 +117,11 @@ The block is executed and the execution time is stored as a set of fields in the
currently running transaction. If no transaction is present the block is yielded
without measuring anything.
-3 values are measured for a block:
+Three values are measured for a block:
-1. The real time elapsed, stored in NAME_real_time.
-2. The CPU time elapsed, stored in NAME_cpu_time.
-3. The call count, stored in NAME_call_count.
+- The real time elapsed, stored in NAME_real_time.
+- The CPU time elapsed, stored in NAME_cpu_time.
+- The call count, stored in NAME_call_count.
Both the real and CPU timings are measured in milliseconds.
diff --git a/doc/development/logging.md b/doc/development/logging.md
new file mode 100644
index 00000000000..abd08c420da
--- /dev/null
+++ b/doc/development/logging.md
@@ -0,0 +1,144 @@
+# GitLab Developers Guide to Logging
+
+[GitLab Logs](../administration/logs.md) play a critical role for both
+administrators and GitLab team members to diagnose problems in the field.
+
+## Don't use `Rails.logger`
+
+Currently `Rails.logger` calls all get saved into `production.log`, which contains
+a mix of Rails' logs and other calls developers have inserted in the code base.
+For example:
+
+```
+Started GET "/gitlabhq/yaml_db/tree/master" for 168.111.56.1 at 2015-02-12 19:34:53 +0200
+Processing by Projects::TreeController#show as HTML
+ Parameters: {"project_id"=>"gitlabhq/yaml_db", "id"=>"master"}
+
+ ...
+
+ Namespaces"."created_at" DESC, "namespaces"."id" DESC LIMIT 1 [["id", 26]]
+ CACHE (0.0ms) SELECT "members".* FROM "members" WHERE "members"."source_type" = 'Project' AND "members"."type" IN ('ProjectMember') AND "members"."source_id" = $1 AND "members"."source_type" = $2 AND "members"."user_id" = 1 ORDER BY "members"."created_at" DESC, "members"."id" DESC LIMIT 1 [["source_id", 18], ["source_type", "Project"]]
+ CACHE (0.0ms) SELECT "members".* FROM "members" WHERE "members"."source_type" = 'Project' AND "members".
+ (1.4ms) SELECT COUNT(*) FROM "merge_requests" WHERE "merge_requests"."target_project_id" = $1 AND ("merge_requests"."state" IN ('opened','reopened')) [["target_project_id", 18]]
+ Rendered layouts/nav/_project.html.haml (28.0ms)
+ Rendered layouts/_collapse_button.html.haml (0.2ms)
+ Rendered layouts/_flash.html.haml (0.1ms)
+ Rendered layouts/_page.html.haml (32.9ms)
+Completed 200 OK in 166ms (Views: 117.4ms | ActiveRecord: 27.2ms)
+```
+
+These logs suffer from a number of problems:
+
+1. They often lack timestamps or other contextual information (e.g. project ID, user)
+2. They may span multiple lines, which make them hard to find via Elasticsearch.
+3. They lack a common structure, which make them hard to parse by log
+forwarders, such as Logstash or Fluentd. This also makes them hard to
+search.
+
+Note that currently on GitLab.com, any messages in `production.log` will
+NOT get indexed by Elasticsearch due to the sheer volume and noise. They
+do end up in Google Stackdriver, but it is still harder to search for
+logs there. See the [GitLab.com logging
+documentation](https://gitlab.com/gitlab-com/runbooks/blob/master/howto/logging.md)
+for more details.
+
+## Use structured (JSON) logging
+
+Structured logging solves these problems. Consider the example from an API request:
+
+```json
+{"time":"2018-10-29T12:49:42.123Z","severity":"INFO","duration":709.08,"db":14.59,"view":694.49,"status":200,"method":"GET","path":"/api/v4/projects","params":[{"key":"action","value":"git-upload-pack"},{"key":"changes","value":"_any"},{"key":"key_id","value":"secret"},{"key":"secret_token","value":"[FILTERED]"}],"host":"localhost","ip":"::1","ua":"Ruby","route":"/api/:version/projects","user_id":1,"username":"root","queue_duration":100.31,"gitaly_calls":30}
+```
+
+In a single line, we've included all the information that a user needs
+to understand what happened: the timestamp, HTTP method and path, user
+ID, etc.
+
+### How to use JSON logging
+
+Suppose you want to log the events that happen in a project
+importer. You want to log issues created, merge requests, etc. as the
+importer progresses. Here's what to do:
+
+1. Look at [the list of GitLab Logs](../administration/logs.md) to see
+if your log message might belong with one of the existing log files.
+1. If there isn't a good place, consider creating a new filename, but
+check with a maintainer if it makes sense to do so. A log file should
+make it easy for people to search pertinent logs in one place. For
+example, `geo.log` contains all logs pertaining to GitLab Geo.
+To create a new file:
+ 1. Choose a filename (e.g. `importer_json.log`).
+ 1. Create a new subclass of `Gitlab::JsonLogger`:
+
+ ```ruby
+ module Gitlab
+ module Import
+ class Logger < ::Gitlab::JsonLogger
+ def self.file_name_noext
+ 'importer_json'
+ end
+ end
+ end
+ end
+ ```
+
+ 1. In your class where you want to log, you might initialize the logger as an instance variable:
+
+ ```ruby
+ attr_accessor :logger
+
+ def initialize
+ @logger = Gitlab::Import::Logger.build
+ end
+ ```
+
+ Note that it's useful to memoize this because creating a new logger
+ each time you log will open a file, adding unnecessary overhead.
+
+1. Now insert log messages into your code. When adding logs,
+ make sure to include all the context as key-value pairs:
+
+ ```ruby
+ # BAD
+ logger.info("Unable to create project #{project.id}")
+ ```
+
+ ```ruby
+ # GOOD
+ logger.info("Unable to create project", project_id: project.id)
+ ```
+
+1. Be sure to create a common base structure of your log messages. For example,
+ all messages might have `current_user_id` and `project_id` to make it easier
+ to search for activities by user for a given time.
+
+1. Do NOT mix and match types. Elasticsearch won't be able to index your
+ logs properly if you [mix integer and string
+ types](https://www.elastic.co/guide/en/elasticsearch/guide/current/mapping.html#_avoiding_type_gotchas):
+
+ ```ruby
+ # BAD
+ logger.info("Import error", error: 1)
+ logger.info("Import error", error: "I/O failure")
+ ```
+
+ ```ruby
+ # GOOD
+ logger.info("Import error", error_code: 1, error: "I/O failure")
+ ```
+
+## Additional steps with new log files
+
+1. Consider log retention settings. By default, Omnibus will rotate any
+logs in `/var/log/gitlab/gitlab-rails/*.log` every hour and [keep at
+most 30 compressed files](https://docs.gitlab.com/omnibus/settings/logs.html#logrotate).
+On GitLab.com, that setting is only 6 compressed files. These settings should suffice
+for most users, but you may need to tweak them in [omnibus-gitlab](https://gitlab.com/gitlab-org/omnibus-gitlab).
+
+1. If you add a new file, submit an issue to the [production
+tracker](https://gitlab.com/gitlab-com/gl-infra/production/issues) or
+a merge request to the [gitlab_fluentd](https://gitlab.com/gitlab-cookbooks/gitlab_fluentd)
+project. See [this example](https://gitlab.com/gitlab-cookbooks/gitlab_fluentd/merge_requests/51/diffs).
+
+1. Be sure to update the [GitLab CE/EE documentation](../administration/logs.md) and the [GitLab.com
+runbooks](https://gitlab.com/gitlab-com/runbooks/blob/master/howto/logging.md).
diff --git a/doc/development/new_fe_guide/development/performance.md b/doc/development/new_fe_guide/development/performance.md
index 244dfb3756f..5ccd5357314 100644
--- a/doc/development/new_fe_guide/development/performance.md
+++ b/doc/development/new_fe_guide/development/performance.md
@@ -2,7 +2,7 @@
## Monitoring
-We have a performance dashboard available in one of our [grafana instances](https://performance.gprd.gitlab.com/dashboard/db/sitespeed-page-summary?orgId=1). This dashboard automatically aggregates metric data from [sitespeed.io](https://sitespeed.io) every 6 hours. These changes are displayed after a set number of pages are aggregated.
+We have a performance dashboard available in one of our [grafana instances](https://dashboards.gitlab.net/d/1EBTz3Dmz/sitespeed-page-summary?orgId=1). This dashboard automatically aggregates metric data from [sitespeed.io](https://sitespeed.io) every 6 hours. These changes are displayed after a set number of pages are aggregated.
These pages can be found inside a text file in the gitlab-build-images [repository](https://gitlab.com/gitlab-org/gitlab-build-images) called [gitlab.txt](https://gitlab.com/gitlab-org/gitlab-build-images/blob/master/scripts/gitlab.txt)
Any frontend engineer can contribute to this dashboard. They can contribute by adding or removing urls of pages from this text file. Please have a [frontend monitoring expert](https://about.gitlab.com/team) review your changes before assigning to a maintainer of the `gitlab-build-images` project. The changes will go live on the next scheduled run after the changes are merged into `master`.
diff --git a/doc/development/new_fe_guide/development/testing.md b/doc/development/new_fe_guide/development/testing.md
index a9223ac6b0f..082acbedcd2 100644
--- a/doc/development/new_fe_guide/development/testing.md
+++ b/doc/development/new_fe_guide/development/testing.md
@@ -1,30 +1,247 @@
# Overview of Frontend Testing
-## Types of tests in our codebase
+Tests relevant for frontend development can be found at two places:
+
+- `spec/javascripts/` which are run by Karma and contain
+ - [frontend unit tests](#frontend-unit-tests)
+ - [frontend component tests](#frontend-component-tests)
+ - [frontend integration tests](#frontend-integration-tests)
+- `spec/features/` which are run by RSpec and contain
+ - [feature tests](#feature-tests)
+
+In addition there were feature tests in `features/` run by Spinach in the past.
+These have been removed from our codebase in May 2018 ([#23036](https://gitlab.com/gitlab-org/gitlab-ce/issues/23036)).
+
+See also:
-* **RSpec**
- * **[Ruby unit tests](#ruby-unit-tests-spec-rb)** for models, controllers, helpers, etc. (`/spec/**/*.rb`)
- * **[Full feature tests](#full-feature-tests-spec-features-rb)** (`/spec/features/**/*.rb`)
-* **[Karma](#karma-tests-spec-javascripts-js)** (`/spec/javascripts/**/*.js`)
-* <s>Spinach</s> — These have been removed from our codebase in May 2018. (`/features/`)
+- [old testing guide](../../testing_guide/frontend_testing.html)
+- [notes on testing Vue components](../../fe_guide/vue.html#testing-vue-components)
-## RSpec: Ruby unit tests `/spec/**/*.rb`
+## Frontend unit tests
-These tests are meant to unit test the ruby models, controllers and helpers.
+Unit tests are on the lowest abstraction level and typically test functionality that is not directly perceivable by a user.
-### When do we write/update these tests?
+### When to use unit tests
-Whenever we create or modify any Ruby models, controllers or helpers we add/update corresponding tests.
+<details>
+ <summary>exported functions and classes</summary>
+ Anything that is exported can be reused at various places in a way you have no control over.
+ Therefore it is necessary to document the expected behavior of the public interface with tests.
+</details>
----
+<details>
+ <summary>Vuex actions</summary>
+ Any Vuex action needs to work in a consistent way independent of the component it is triggered from.
+</details>
-## RSpec: Full feature tests `/spec/features/**/*.rb`
+<details>
+ <summary>Vuex mutations</summary>
+ For complex Vuex mutations it helps to identify the source of a problem by separating the tests from other parts of the Vuex store.
+</details>
-Full feature tests will load a full app environment and allow us to test things like rendering DOM, interacting with links and buttons, testing the outcome of those interactions through multiple pages if necessary. These are also called end-to-end tests but should not be confused with QA end-to-end tests (`package-and-qa` manual pipeline job).
+### When *not* to use unit tests
-### When do we write/update these tests?
+<details>
+ <summary>non-exported functions or classes</summary>
+ Anything that is not exported from a module can be considered private or an implementation detail and doesn't need to be tested.
+</details>
-When we add a new feature, we write at least two tests covering the success and the failure scenarios.
+<details>
+ <summary>constants</summary>
+ Testing the value of a constant would mean to copy it.
+ This results in extra effort without additional confidence that the value is correct.
+</details>
+
+<details>
+ <summary>Vue components</summary>
+ Computed properties, methods, and lifecycle hooks can be considered an implementation detail of components and don't need to be tested.
+ They are implicitly covered by component tests.
+ The <a href="https://vue-test-utils.vuejs.org/guides/#getting-started">official Vue guidelines</a> suggest the same.
+</details>
+
+### What to mock in unit tests
+
+<details>
+ <summary>state of the class under test</summary>
+ Modifying the state of the class under test directly rather than using methods of the class avoids side-effects in test setup.
+</details>
+
+<details>
+ <summary>other exported classes</summary>
+ Every class needs to be tested in isolation to prevent test scenarios from growing exponentially.
+</details>
+
+<details>
+ <summary>single DOM elements if passed as parameters</summary>
+ For tests that only operate on single DOM elements rather than a whole page, creating these elements is cheaper than loading a whole HTML fixture.
+</details>
+
+<details>
+ <summary>all server requests</summary>
+ When running frontend unit tests, the backend may not be reachable.
+ Therefore all outgoing requests need to be mocked.
+</details>
+
+<details>
+ <summary>asynchronous background operations</summary>
+ Background operations cannot be stopped or waited on, so they will continue running in the following tests and cause side effects.
+</details>
+
+### What *not* to mock in unit tests
+
+<details>
+ <summary>non-exported functions or classes</summary>
+ Everything that is not exported can be considered private to the module and will be implicitly tested via the exported classes / functions.
+</details>
+
+<details>
+ <summary>methods of the class under test</summary>
+ By mocking methods of the class under test, the mocks will be tested and not the real methods.
+</details>
+
+<details>
+ <summary>utility functions (pure functions, or those that only modify parameters)</summary>
+ If a function has no side effects because it has no state, it is safe to not mock it in tests.
+</details>
+
+<details>
+ <summary>full HTML pages</summary>
+ Loading the HTML of a full page slows down tests, so it should be avoided in unit tests.
+</details>
+
+## Frontend component tests
+
+Component tests cover the state of a single component that is perceivable by a user depending on external signals such as user input, events fired from other components, or application state.
+
+### When to use component tests
+
+- Vue components
+
+### When *not* to use component tests
+
+<details>
+ <summary>Vue applications</summary>
+ Vue applications may contain many components.
+ Testing them on a component level requires too much effort.
+ Therefore they are tested on frontend integration level.
+</details>
+
+<details>
+ <summary>HAML templates</summary>
+ HAML templates contain only Markup and no frontend-side logic.
+ Therefore they are not complete components.
+</details>
+
+### What to mock in component tests
+
+<details>
+ <summary>DOM</summary>
+ Operating on the real DOM is significantly slower than on the virtual DOM.
+</details>
+
+<details>
+ <summary>properties and state of the component under test</summary>
+ Similarly to testing classes, modifying the properties directly (rather than relying on methods of the component) avoids side-effects.
+</details>
+
+<details>
+ <summary>Vuex store</summary>
+ To avoid side effects and keep component tests simple, Vuex stores are replaced with mocks.
+</details>
+
+<details>
+ <summary>all server requests</summary>
+ Similar to unit tests, when running component tests, the backend may not be reachable.
+ Therefore all outgoing requests need to be mocked.
+</details>
+
+<details>
+ <summary>asynchronous background operations</summary>
+ Similar to unit tests, background operations cannot be stopped or waited on, so they will continue running in the following tests and cause side effects.
+</details>
+
+<details>
+ <summary>child components</summary>
+ Every component is tested individually, so child components are mocked.
+ See also <a href="https://vue-test-utils.vuejs.org/api/#shallowmount">shallowMount()</a>
+</details>
+
+### What *not* to mock in component tests
+
+<details>
+ <summary>methods or computed properties of the component under test</summary>
+ By mocking part of the component under test, the mocks will be tested and not the real component.
+</details>
+
+<details>
+ <summary>functions and classes independent from Vue</summary>
+ All plain JavaScript code is already covered by unit tests and needs not to be mocked in component tests.
+</details>
+
+## Frontend integration tests
+
+Integration tests cover the interaction between all components on a single page.
+Their abstraction level is comparable to how a user would interact with the UI.
+
+### When to use integration tests
+
+<details>
+ <summary>page bundles (<code>index.js</code> files in <code>app/assets/javascripts/pages/</code>)</summary>
+ Testing the page bundles ensures the corresponding frontend components integrate well.
+</details>
+
+<details>
+ <summary>Vue applications outside of page bundles</summary>
+ Testing Vue applications as a whole ensures the corresponding frontend components integrate well.
+</details>
+
+### What to mock in integration tests
+
+<details>
+ <summary>HAML views (use fixtures instead)</summary>
+ Rendering HAML views requires a Rails environment including a running database which we cannot rely on in frontend tests.
+</details>
+
+<details>
+ <summary>all server requests</summary>
+ Similar to unit and component tests, when running component tests, the backend may not be reachable.
+ Therefore all outgoing requests need to be mocked.
+</details>
+
+<details>
+ <summary>asynchronous background operations that are not perceivable on the page</summary>
+ Background operations that affect the page need to be tested on this level.
+ All other background operations cannot be stopped or waited on, so they will continue running in the following tests and cause side effects.
+</details>
+
+### What *not* to mock in integration tests
+
+<details>
+ <summary>DOM</summary>
+ Testing on the real DOM ensures our components work in the environment they are meant for.
+ Part of this will be delegated to <a href="https://gitlab.com/gitlab-org/quality/team-tasks/issues/45">cross-browser testing</a>.
+</details>
+
+<details>
+ <summary>properties or state of components</summary>
+ On this level, all tests can only perform actions a user would do.
+ For example to change the state of a component, a click event would be fired.
+</details>
+
+<details>
+ <summary>Vuex stores</summary>
+ When testing the frontend code of a page as a whole, the interaction between Vue components and Vuex stores is covered as well.
+</details>
+
+## Feature tests
+
+In contrast to [frontend integration tests](#frontend-integration-tests), feature tests make requests against the real backend instead of using fixtures.
+This also implies that database queries are executed which makes this category significantly slower.
+
+### When to use feature tests
+
+- use cases that require a backend and cannot be tested using fixtures
+- behavior that is not part of a page bundle but defined globally
### Relevant notes
@@ -48,33 +265,9 @@ wait_for_requests
expect(page).not_to have_selector('.card')
```
----
-
-## Karma tests `/spec/javascripts/**/*.js`
-
-These are the more frontend-focused, at the moment. They're **faster** than `rspec` and make for very quick testing of frontend components.
-
-### When do we write/update these tests?
-
-When we add/update a method/action/mutation to Vue or Vuex, we write karma tests to ensure the logic we wrote doesn't break. We should, however, refrain from writing tests that double-test Vue's internal features.
-
-### Relevant notes
-
-Karma tests are run against a virtual DOM.
+## Test helpers
-To populate the DOM, we can use fixtures to fake the generation of HTML instead of having Rails do that.
-
-Be sure to check the [best practices for karma tests](../../testing_guide/frontend_testing.html#best-practices).
-
-### Vue and Vuex
-
-Test as much as possible without double-testing Vue's internal features, as mentioned above.
-
-Make sure to test computedProperties, mutations, actions. Run the action and test that the proper mutations are committed.
-
-Also check these [notes on testing Vue components](../../fe_guide/vue.html#testing-vue-components).
-
-#### Vuex Helper: `testAction`
+### Vuex Helper: `testAction`
We have a helper available to make testing actions easier, as per [official documentation](https://vuex.vuejs.org/en/testing.html):
@@ -97,7 +290,7 @@ testAction(
Check an example in [spec/javascripts/ide/stores/actions_spec.jsspec/javascripts/ide/stores/actions_spec.js](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/javascripts/ide/stores/actions_spec.js).
-#### Vue Helper: `mountComponent`
+### Vue Helper: `mountComponent`
To make mounting a Vue component easier and more readable, we have a few helpers available in `spec/helpers/vue_mount_component_helper`.
@@ -133,6 +326,7 @@ afterEach(() => {
vm.$destroy();
});
```
+
## Testing with older browsers
Some regressions only affect a specific browser version. We can install and test in particular browsers with either Firefox or Browserstack using the following steps:
@@ -140,20 +334,21 @@ Some regressions only affect a specific browser version. We can install and test
### Browserstack
-[Browserstack](https://www.browserstack.com/) allows you to test more than 1200 mobile devices and browsers.
+[Browserstack](https://www.browserstack.com/) allows you to test more than 1200 mobile devices and browsers.
You can use it directly through the [live app](https://www.browserstack.com/live) or you can install the [chrome extension](https://chrome.google.com/webstore/detail/browserstack/nkihdmlheodkdfojglpcjjmioefjahjb) for easy access.
You can find the credentials on 1Password, under `frontendteam@gitlab.com`.
### Firefox
#### macOS
+
You can download any older version of Firefox from the releases FTP server, https://ftp.mozilla.org/pub/firefox/releases/
1. From the website, select a version, in this case `50.0.1`.
-2. Go to the mac folder.
-3. Select your preferred language, you will find the dmg package inside, download it.
-4. Drag and drop the application to any other folder but the `Applications` folder.
-5. Rename the application to something like `Firefox_Old`.
-6. Move the application to the `Applications` folder.
-7. Open up a terminal and run `/Applications/Firefox_Old.app/Contents/MacOS/firefox-bin -profilemanager` to create a new profile specific to that Firefox version.
-8. Once the profile has been created, quit the app, and run it again like normal. You now have a working older Firefox version.
+1. Go to the mac folder.
+1. Select your preferred language, you will find the dmg package inside, download it.
+1. Drag and drop the application to any other folder but the `Applications` folder.
+1. Rename the application to something like `Firefox_Old`.
+1. Move the application to the `Applications` folder.
+1. Open up a terminal and run `/Applications/Firefox_Old.app/Contents/MacOS/firefox-bin -profilemanager` to create a new profile specific to that Firefox version.
+1. Once the profile has been created, quit the app, and run it again like normal. You now have a working older Firefox version.
diff --git a/doc/development/performance.md b/doc/development/performance.md
index c7b10dfd5ce..4cc2fdc9a58 100644
--- a/doc/development/performance.md
+++ b/doc/development/performance.md
@@ -9,17 +9,17 @@ The process of solving performance problems is roughly as follows:
1. Make sure there's an issue open somewhere (e.g., on the GitLab CE issue
tracker), create one if there isn't. See [#15607][#15607] for an example.
-2. Measure the performance of the code in a production environment such as
+1. Measure the performance of the code in a production environment such as
GitLab.com (see the [Tooling](#tooling) section below). Performance should be
measured over a period of _at least_ 24 hours.
-3. Add your findings based on the measurement period (screenshots of graphs,
+1. Add your findings based on the measurement period (screenshots of graphs,
timings, etc) to the issue mentioned in step 1.
-4. Solve the problem.
-5. Create a merge request, assign the "Performance" label and assign it to
+1. Solve the problem.
+1. Create a merge request, assign the "Performance" label and assign it to
[@yorickpeterse][yorickpeterse] for reviewing.
-6. Once a change has been deployed make sure to _again_ measure for at least 24
+1. Once a change has been deployed make sure to _again_ measure for at least 24
hours to see if your changes have any impact on the production environment.
-7. Repeat until you're done.
+1. Repeat until you're done.
When providing timings make sure to provide:
@@ -34,13 +34,14 @@ graphs/dashboards.
## Tooling
-GitLab provides built-in tools to aid the process of improving performance:
+GitLab provides built-in tools to help improve performance and availability:
* [Profiling](profiling.md)
* [Sherlock](profiling.md#sherlock)
* [GitLab Performance Monitoring](../administration/monitoring/performance/index.md)
* [Request Profiling](../administration/monitoring/performance/request_profiling.md)
* [QueryRecoder](query_recorder.md) for preventing `N+1` regressions
+* [Chaos endpoints](chaos_endpoints.md) for testing failure scenarios. Intended mainly for testing availability.
GitLab employees can use GitLab.com's performance monitoring systems located at
<https://dashboards.gitlab.net>, this requires you to log in using your
@@ -93,14 +94,14 @@ result of this should be used instead of the `Benchmark` module.
In short:
-1. Don't trust benchmarks you find on the internet.
-2. Never make claims based on just benchmarks, always measure in production to
+- Don't trust benchmarks you find on the internet.
+- Never make claims based on just benchmarks, always measure in production to
confirm your findings.
-3. X being N times faster than Y is meaningless if you don't know what impact it
+- X being N times faster than Y is meaningless if you don't know what impact it
will actually have on your production environment.
-4. A production environment is the _only_ benchmark that always tells the truth
+- A production environment is the _only_ benchmark that always tells the truth
(unless your performance monitoring systems are not set up correctly).
-5. If you must write a benchmark use the benchmark-ips Gem instead of Ruby's
+- If you must write a benchmark use the benchmark-ips Gem instead of Ruby's
`Benchmark` module.
## Profiling
diff --git a/doc/development/post_deployment_migrations.md b/doc/development/post_deployment_migrations.md
index cfc91539bee..5986efa9974 100644
--- a/doc/development/post_deployment_migrations.md
+++ b/doc/development/post_deployment_migrations.md
@@ -57,13 +57,13 @@ depends on this column being present while it's running. Normally you'd follow
these steps in such a case:
1. Stop the GitLab instance
-2. Run the migration removing the column
-3. Start the GitLab instance again
+1. Run the migration removing the column
+1. Start the GitLab instance again
Using post deployment migrations we can instead follow these steps:
1. Deploy a new version of GitLab while ignoring post deployment migrations
-2. Re-run `rake db:migrate` but without the environment variable set
+1. Re-run `rake db:migrate` but without the environment variable set
Here we don't need any downtime as the migration takes place _after_ a new
version (which doesn't depend on the column anymore) has been deployed.
diff --git a/doc/development/query_count_limits.md b/doc/development/query_count_limits.md
index 310e3faf61b..b3ecaf30d8a 100644
--- a/doc/development/query_count_limits.md
+++ b/doc/development/query_count_limits.md
@@ -8,8 +8,8 @@ in test environments we'll raise an error when this threshold is exceeded.
When a test fails because it executes more than 100 SQL queries there are two
solutions to this problem:
-1. Reduce the number of SQL queries that are executed.
-2. Whitelist the controller or API endpoint.
+- Reduce the number of SQL queries that are executed.
+- Whitelist the controller or API endpoint.
You should only resort to whitelisting when an existing controller or endpoint
is to blame as in this case reducing the number of SQL queries can take a lot of
diff --git a/doc/development/rolling_out_changes_using_feature_flags.md b/doc/development/rolling_out_changes_using_feature_flags.md
index dada59ce242..b65fbc9d958 100644
--- a/doc/development/rolling_out_changes_using_feature_flags.md
+++ b/doc/development/rolling_out_changes_using_feature_flags.md
@@ -152,14 +152,31 @@ in RC1, followed by the feature flag being removed in RC2. This in turn means
the feature will be stable by the time we publish a stable package around the
22nd of the month.
-## Undefined feature flags default to "on"
+## Implicit feature flags
-By default, the [`Project#feature_available?`][project-fa],
+The [`Project#feature_available?`][project-fa],
[`Namespace#feature_available?`][namespace-fa] (EE), and
-[`License.feature_available?`][license-fa] (EE) methods will check if the
-specified feature is behind a feature flag. Unless the feature is explicitly
-disabled or limited to a percentage of users, the feature flag check will
-default to `true`.
+[`License.feature_available?`][license-fa] (EE) methods all implicitly check for
+a feature flag by the same name as the provided argument.
+
+For example if a feature is license-gated, there's no need to add an additional
+explicit feature flag check since the flag will be checked as part of the
+`License.feature_available?` call. Similarly, there's no need to "clean up" a
+feature flag once the feature has reached general availability.
+
+You'd still want to use an explicit `Feature.enabled?` check if your new feature
+isn't gated by a License or Plan.
+
+[project-fa]: https://gitlab.com/gitlab-org/gitlab-ee/blob/4cc1c62918aa4c31750cb21dfb1a6c3492d71080/app/models/project_feature.rb#L63-68
+[namespace-fa]: https://gitlab.com/gitlab-org/gitlab-ee/blob/4cc1c62918aa4c31750cb21dfb1a6c3492d71080/ee/app/models/ee/namespace.rb#L71-85
+[license-fa]: https://gitlab.com/gitlab-org/gitlab-ee/blob/4cc1c62918aa4c31750cb21dfb1a6c3492d71080/ee/app/models/license.rb#L293-300
+
+### Undefined feature flags default to "on"
+
+An important side-effect of the [implicit feature
+flags][#implicit-feature-flags] mentioned above is that unless the feature is
+explicitly disabled or limited to a percentage of users, the feature flag check
+will default to `true`.
As an example, if you were to ship the backend half of a feature behind a flag,
you'd want to explicitly disable that flag until the frontend half is also ready
@@ -171,7 +188,3 @@ to be shipped. You can do this via ChatOps:
Note that you can do this at any time, even before the merge request using the
flag has been merged!
-
-[project-fa]: https://gitlab.com/gitlab-org/gitlab-ee/blob/4cc1c62918aa4c31750cb21dfb1a6c3492d71080/app/models/project_feature.rb#L63-68
-[namespace-fa]: https://gitlab.com/gitlab-org/gitlab-ee/blob/4cc1c62918aa4c31750cb21dfb1a6c3492d71080/ee/app/models/ee/namespace.rb#L71-85
-[license-fa]: https://gitlab.com/gitlab-org/gitlab-ee/blob/4cc1c62918aa4c31750cb21dfb1a6c3492d71080/ee/app/models/license.rb#L293-300
diff --git a/doc/development/swapping_tables.md b/doc/development/swapping_tables.md
index 6b990ece72c..29cd6a43aff 100644
--- a/doc/development/swapping_tables.md
+++ b/doc/development/swapping_tables.md
@@ -8,8 +8,8 @@ Let's say you want to swap the table "events" with "events_for_migration". In
this case you need to follow 3 steps:
1. Rename "events" to "events_temporary"
-2. Rename "events_for_migration" to "events"
-3. Rename "events_temporary" to "events_for_migration"
+1. Rename "events_for_migration" to "events"
+1. Rename "events_temporary" to "events_for_migration"
Rails allows you to do this using the `rename_table` method:
diff --git a/doc/development/switching_to_rails5.md b/doc/development/switching_to_rails5.md
new file mode 100644
index 00000000000..c9a4ce1a1d1
--- /dev/null
+++ b/doc/development/switching_to_rails5.md
@@ -0,0 +1,27 @@
+# Switching to Rails 5
+
+GitLab switched recently to Rails 5. This is a big change (especially for backend development) and it introduces couple of temporary inconveniences.
+
+## After the switch, I found a broken feature. What do I do?
+
+Many fixes and tweaks were done to make our codebase compatible with Rails 5, but it's possible that not all issues were found. If you find an bug, please create an issue and assign it the ~rails5 label.
+
+## It takes much longer to run CI pipelines that build GitLab. Why?
+
+We are temporarily running CI pipelines with Rails 4 and 5 so that we ensure we remain compatible with Rails 4 in case we must revert back to Rails 4 from Rails 5 (this can double the duration of CI pipelines).
+
+We might revert back to Rails 4 if we found a major issue we were unable to quickly fix.
+
+Once we are sure we can stay with Rails 5, we will stop running CI pipelines with Rails 4.
+
+## Can I skip running Rails 4 tests?
+
+If you are sure that your merge request doesn't introduce any incompatibility, you can just include `norails4` anywhere in your branch name and Rails 4 tests will be skipped.
+
+## CI is failing on my test with Rails 4. How can I debug it?
+
+You can run specs locally with Rails 4 using the following command:
+
+```sh
+BUNDLE_GEMFILE=Gemfile.rails4 RAILS5=0 bundle exec rspec ...
+```
diff --git a/doc/development/testing_guide/end_to_end_tests.md b/doc/development/testing_guide/end_to_end_tests.md
index 21ec926414d..e9f236c6b3a 100644
--- a/doc/development/testing_guide/end_to_end_tests.md
+++ b/doc/development/testing_guide/end_to_end_tests.md
@@ -1,32 +1,37 @@
-# End-to-End Testing
+# End-to-end Testing
-## What is End-to-End testing?
+## What is end-to-end testing?
-End-to-End testing is a strategy used to check whether your application works
-as expected across entire software stack and architecture, including
-integration of all microservices and components that are supposed to work
+End-to-end testing is a strategy used to check whether your application works
+as expected across the entire software stack and architecture, including
+integration of all micro-services and components that are supposed to work
together.
## How do we test GitLab?
We use [Omnibus GitLab][omnibus-gitlab] to build GitLab packages and then we
-test these packages using [GitLab QA][gitlab-qa] project, which is entirely
-black-box, click-driven testing framework.
+test these packages using the [GitLab QA orchestrator][gitlab-qa] tool, which is
+a black-box testing framework for the API and the UI.
### Testing nightly builds
We run scheduled pipeline each night to test nightly builds created by Omnibus.
-You can find these nightly pipelines at [GitLab QA pipelines page][gitlab-qa-pipelines].
+You can find these nightly pipelines at [gitlab-org/quality/nightly/pipelines][quality-nightly-pipelines].
+
+### Testing staging
+
+We run scheduled pipeline each night to test staging.
+You can find these nightly pipelines at [gitlab-org/quality/staging/pipelines][quality-staging-pipelines].
### Testing code in merge requests
It is possible to run end-to-end tests (eventually being run within a
[GitLab QA pipeline][gitlab-qa-pipelines]) for a merge request by triggering
-the `package-and-qa` manual action, that should be present in a merge request
-widget.
+the `package-and-qa` manual action in the `test` stage, that should be present
+in a merge request widget (unless the merge request is from a fork).
Manual action that starts end-to-end tests is also available in merge requests
-in Omnibus GitLab project.
+in [Omnibus GitLab][omnibus-gitlab].
Below you can read more about how to use it and how does it work.
@@ -35,46 +40,56 @@ Below you can read more about how to use it and how does it work.
Currently, we are using _multi-project pipeline_-like approach to run QA
pipelines.
-1. Developer triggers a manual action, that can be found in CE and EE merge
+1. Developer triggers a manual action, that can be found in CE / EE merge
requests. This starts a chain of pipelines in multiple projects.
-1. The script being executed triggers a pipeline in GitLab Omnibus and waits
-for the resulting status. We call this a _status attribution_.
+1. The script being executed triggers a pipeline in [Omnibus GitLab][omnibus-gitlab]
+and waits for the resulting status. We call this a _status attribution_.
-1. GitLab packages are being built in Omnibus pipeline. Packages are going to be
-pushed to Container Registry.
+1. GitLab packages are being built in the [Omnibus GitLab][omnibus-gitlab]
+pipeline. Packages are then pushed to its Container Registry.
1. When packages are ready, and available in the registry, a final step in the
-pipeline, that is now running in Omnibus, triggers a new pipeline in the GitLab
-QA project. It also waits for a resulting status.
+[Omnibus GitLab][omnibus-gitlab] pipeline, triggers a new
+[GitLab QA pipeline][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 is being propagated upstream, through
-Omnibus, back to CE / EE merge request.
+1. The result of the [GitLab QA pipeline][gitlab-qa-pipelines] is being
+propagated upstream, through Omnibus, back to the CE / EE merge request.
#### How do I write tests?
In order to write new tests, you first need to learn more about GitLab QA
-architecture. See the [documentation about it][gitlab-qa-architecture] in
-GitLab QA project.
+architecture. See the [documentation about it][gitlab-qa-architecture].
-Once you decided where to put test environment orchestration scenarios and
-instance specs, take a look at the [relevant documentation][instance-qa-readme]
-and examples in [the `qa/` directory][instance-qa-examples].
+Once you decided where to put [test environment orchestration scenarios] and
+[instance-level scenarios], take a look at the [GitLab QA README][instance-qa-readme],
+the [GitLab QA orchestrator README][gitlab-qa-readme], and [the already existing
+instance-level scenarios][instance-level scenarios].
## Where can I ask for help?
You can ask question in the `#quality` channel on Slack (GitLab internal) or
you can find an issue you would like to work on in
-[the issue tracker][gitlab-qa-issues] and start a new discussion there.
+[the `gitlab-ce` issue tracker][gitlab-ce-issues],
+[the `gitlab-ee` issue tracker][gitlab-ce-issues], or
+[the `gitlab-qa` issue tracker][gitlab-qa-issues].
[omnibus-gitlab]: https://gitlab.com/gitlab-org/omnibus-gitlab
[gitlab-qa]: https://gitlab.com/gitlab-org/gitlab-qa
+[gitlab-qa-readme]: https://gitlab.com/gitlab-org/gitlab-qa/tree/master/README.md
[gitlab-qa-pipelines]: https://gitlab.com/gitlab-org/gitlab-qa/pipelines
+[quality-nightly-pipelines]: https://gitlab.com/gitlab-org/quality/nightly/pipelines
+[quality-staging-pipelines]: https://gitlab.com/gitlab-org/quality/staging/pipelines
[gitlab-qa-architecture]: https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/architecture.md
-[gitlab-qa-issues]: https://gitlab.com/gitlab-org/gitlab-qa/issues
+[gitlab-qa-issues]: https://gitlab.com/gitlab-org/gitlab-qa/issues?label_name%5B%5D=new+scenario
+[gitlab-ce-issues]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name[]=QA&label_name[]=test
+[gitlab-ee-issues]: https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name[]=QA&label_name[]=test
+[test environment orchestration scenarios]: https://gitlab.com/gitlab-org/gitlab-qa/tree/master/lib/gitlab/qa/scenario
+[instance-level scenarios]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa/qa/specs/features
+[Page objects documentation]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa/qa/page/README.md
[instance-qa-readme]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa/README.md
[instance-qa-examples]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa/qa
diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md
index 4f4ff85fe1d..36d150c8a5b 100644
--- a/doc/development/testing_guide/review_apps.md
+++ b/doc/development/testing_guide/review_apps.md
@@ -1,75 +1,82 @@
-# Review apps
+# Review Apps
-Review Apps are automatically deployed by each pipeline, both in
+Review Apps are automatically deployed by each pipeline, both in
[CE](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22010) and
[EE](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6665).
## How does it work?
-1. On every [pipeline][gitlab-pipeline] during the `test` stage, the
+1. On every [pipeline][gitlab-pipeline] during the `test` stage, the
[`review` job][review-job] is automatically started.
1. The `review` job [triggers a pipeline][cng-pipeline] in the
- [`CNG-mirror`][cng-mirror] [^1] project
-1. The `CNG-mirror` pipeline creates the Docker images of each component (e.g. `gitlab-rails-ee`,
- `gitlab-shell`, `gitaly` etc.) based on the commit from the
+ [`CNG-mirror`][cng-mirror] project.
+ - We use the `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. The `CNG-mirror` 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]
-1. Once all images are built, the review app is deployed using
- [the official GitLab Helm chart][helm-chart] [^2] to the
+ [registry][cng-mirror-registry].
+1. Once all images are built, the Review App is deployed using
+ [the official GitLab Helm chart][helm-chart] to the
[`review-apps-ee` Kubernetes cluster on GCP][review-apps-ee]
- - 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 overriden with the images built and stored in the
- [`CNG-mirror` project's registry][cng-mirror-registry]
-1. Once the `review` job succeeds, you should be able to use your review app
+ - 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 overriden 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` job succeeds, you should be able to use your Review App
thanks to the direct link to it from the MR widget. 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**.
+ **gitlab-{ce,ee} Review App's root password** (note that there's currently
+ [a bug where the default password seems to be overriden][password-bug]).
**Additional notes:**
-- The Kubernetes cluster is connected to the `gitlab-ee` project using [GitLab's
- Kubernetes integration][gitlab-k8s-integration]. This basically allows to have
- a link to the review app directly from the merge request widget.
-- The manual `stop_review` in the `post-cleanup` stage can be used to stop a
- review app manually, and is also started by GitLab once a branch is deleted
-- [TBD] Review apps are cleaned up regularly using a pipeline schedule that runs
- the [`scripts/review_apps/automated_cleanup.rb`][automated_cleanup.rb] script
-- If you're unable to log in using the `root` username and password, the
- deployment may have failed. Stop the Review App via the `stop_review`
+- The Kubernetes cluster is connected to the `gitlab-{ce,ee}` projects using
+ [GitLab's Kubernetes integration][gitlab-k8s-integration]. This basically
+ allows to have a link to the Review App directly from the merge request widget.
+- The manual `stop_review` 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
+ the [`scripts/review_apps/automated_cleanup.rb`][automated_cleanup.rb] script.
+- If the Review App deployment fails, you can simply retry it (there's no need
+ to run the `stop_review` job first).
+- If you're unable to log in using the `root` username and password, you may
+ encounter [this bug][password-bug]. Stop the Review App via the `stop_review`
manual job and then retry the `review` job to redeploy the Review App.
-[^1]: We use the `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.
-[^2]: Since we're using [the official GitLab Helm chart][helm-chart], this means
- you get the a dedicated environment for your branch that's very close to what it
- would look in production
-
## Frequently Asked Questions
-**Will it be too much to trigger CNG image builds on every test run? This could create thousands of unused docker images.**
+**Isn't it too much to trigger CNG image builds on every test run? This creates
+thousands of unused Docker images.**
- > We have to start somewhere and improve later. If we see this getting out of hand, we will revisit.
+ > We have to start somewhere and improve later. Also, we're using the
+ CNG-mirror project to store these Docker images so that we can just wipe out
+ the registry at some point, and use a new fresh, empty one.
-**How big is the Kubernetes cluster?**
+**How big are the Kubernetes clusters (`review-apps-ce` and `review-apps-ee`)?**
- > The cluster is currently setup with a single pool of preemptible
- nodes, with a minimum of 1 node and a maximum of 30 nodes.
+ > The clusters are currently set up with a single pool of preemptible nodes,
+ with a minimum of 1 node and a maximum of 100 nodes.
**What are the machine running on the cluster?**
> We're currently using `n1-standard-4` (4 vCPUs, 15 GB memory) machines.
-**How do we secure this from abuse? Apps are open to the world so we need to find a way to limit it to only us.**
+**How do we secure this from abuse? Apps are open to the world so we need to
+find a way to limit it to only us.**
- > This won't work for forks. We will add a root password to 1password shared vault.
+ > This isn't enabled for forks.
-[gitlab-pipeline]: https://gitlab.com/gitlab-org/gitlab-ee/pipelines/29302122
-[review-job]: https://gitlab.com/gitlab-org/gitlab-ee/-/jobs/94294136
+[gitlab-pipeline]: https://gitlab.com/gitlab-org/gitlab-ce/pipelines/35850709
+[review-job]: https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/118076368
[cng-mirror]: https://gitlab.com/gitlab-org/build/CNG-mirror
-[cng-pipeline]: https://gitlab.com/gitlab-org/build/CNG-mirror/pipelines/29307727
+[cng-pipeline]: https://gitlab.com/gitlab-org/build/CNG-mirror/pipelines/35883435
[cng-mirror-registry]: https://gitlab.com/gitlab-org/build/CNG-mirror/container_registry
[helm-chart]: https://gitlab.com/charts/gitlab/
[review-apps-ee]: https://console.cloud.google.com/kubernetes/clusters/details/us-central1-b/review-apps-ee?project=gitlab-review-apps
@@ -77,6 +84,7 @@ Review Apps are automatically deployed by each pipeline, both in
[automated_cleanup.rb]: https://gitlab.com/gitlab-org/gitlab-ee/blob/master/scripts/review_apps/automated_cleanup.rb
[Auto-DevOps.gitlab-ci.yml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
[gitlab-k8s-integration]: https://docs.gitlab.com/ee/user/project/clusters/index.html
+[password-bug]: https://gitlab.com/gitlab-org/gitlab-ce/issues/53621
---
diff --git a/doc/development/testing_guide/smoke.md b/doc/development/testing_guide/smoke.md
index 3f2843cba6e..3360031c220 100644
--- a/doc/development/testing_guide/smoke.md
+++ b/doc/development/testing_guide/smoke.md
@@ -1,8 +1,9 @@
# Smoke Tests
-It is imperative in any testing suite that we have Smoke Tests. In short, smoke tests are will run quick sanity
-end-to-end functional tests from GitLab QA and are designed to run against the specified environment to ensure that
-basic functionality is working.
+It is imperative in any testing suite that we have Smoke Tests. In short, smoke
+tests will run quick sanity end-to-end functional tests from GitLab QA and are
+designed to run against the specified environment to ensure that basic
+functionality is working.
Currently, our suite consists of this basic functionality coverage:
@@ -11,6 +12,8 @@ Currently, our suite consists of this basic functionality coverage:
- Issue Creation
- Merge Request Creation
+Smoke tests have the `:smoke` RSpec metadata.
+
---
[Return to Testing documentation](index.md)
diff --git a/doc/development/testing_guide/testing_levels.md b/doc/development/testing_guide/testing_levels.md
index 32ed22ca3ed..a8671fc3aa3 100644
--- a/doc/development/testing_guide/testing_levels.md
+++ b/doc/development/testing_guide/testing_levels.md
@@ -34,7 +34,11 @@ records should use stubs/doubles as much as possible.
Formal definition: https://en.wikipedia.org/wiki/Integration_testing
-These kind of tests ensure that individual parts of the application work well together, without the overhead of the actual app environment (i.e. the browser). These tests should assert at the request/response level: status code, headers, body. They're useful to test permissions, redirections, what view is rendered etc.
+These kind of tests ensure that individual parts of the application work well
+together, without the overhead of the actual app environment (i.e. the browser).
+These tests should assert at the request/response level: status code, headers,
+body.
+They're useful to test permissions, redirections, what view is rendered etc.
| Code path | Tests path | Testing engine | Notes |
| --------- | ---------- | -------------- | ----- |
@@ -67,20 +71,40 @@ run JavaScript tests, so you can either run unit tests (e.g. test a single
JavaScript method), or integration tests (e.g. test a component that is composed
of multiple components).
-## System tests or feature tests
+## White-box tests at the system level (formerly known as System / Feature tests)
-Formal definition: https://en.wikipedia.org/wiki/System_testing.
+Formal definitions:
-These kind of tests ensure the application works as expected from a user point
-of view (aka black-box testing). These tests should test a happy path for a
-given page or set of pages, and a test case should be added for any regression
+- https://en.wikipedia.org/wiki/System_testing
+- https://en.wikipedia.org/wiki/White-box_testing
+
+These kind of tests ensure the GitLab *Rails* application (i.e.
+`gitlab-ce`/`gitlab-ee`) works as expected from a *browser* point of view.
+
+Note that:
+
+- knowledge of the internals of the application are still required
+- data needed for the tests are usually created directly using RSpec factories
+- expectations are often set on the database or objects state
+
+These tests should only be used when:
+
+- the functionality/component being tested is small
+- the internal state of the objects/database *needs* to be tested
+- it cannot be tested at a lower level
+
+For instance, to test the breadcrumbs on a given page, writing a system test
+makes sense since it's a small component, which cannot be tested at the unit or
+controller level.
+
+Only test the happy path, but make sure to add a test case for any regression
that couldn't have been caught at lower levels with better tests (i.e. if a
regression is found, regression tests should be added at the lowest-level
possible).
| Tests path | Testing engine | Notes |
| ---------- | -------------- | ----- |
-| `spec/features/` | [Capybara] + [RSpec] | If your spec has the `:js` metadata, the browser driver will be [Poltergeist], otherwise it's using [RackTest]. |
+| `spec/features/` | [Capybara] + [RSpec] | If your test has the `:js` metadata, the browser driver will be [Poltergeist], otherwise it's using [RackTest]. |
### Consider **not** writing a system test!
@@ -89,7 +113,7 @@ we have enough Unit & Integration tests), we shouldn't need to duplicate their
thorough testing at the System test level.
It's very easy to add tests, but a lot harder to remove or improve tests, so one
-should take care of not introducing too many (slow and duplicated) specs.
+should take care of not introducing too many (slow and duplicated) tests.
The reasons why we should follow these best practices are as follows:
@@ -107,29 +131,33 @@ The reasons why we should follow these best practices are as follows:
[Poltergeist]: https://github.com/teamcapybara/capybara#poltergeist
[RackTest]: https://github.com/teamcapybara/capybara#racktest
-## Black-box tests or end-to-end tests
+## Black-box tests at the system level, aka end-to-end tests
+
+Formal definitions:
+
+- https://en.wikipedia.org/wiki/System_testing
+- https://en.wikipedia.org/wiki/Black-box_testing
GitLab consists of [multiple pieces] such as [GitLab Shell], [GitLab Workhorse],
[Gitaly], [GitLab Pages], [GitLab Runner], and GitLab Rails. All theses pieces
are configured and packaged by [GitLab Omnibus].
-[GitLab QA] is a tool that allows to test that all these pieces integrate well
-together by building a Docker image for a given version of GitLab Rails and
-running feature tests (i.e. using Capybara) against it.
+The QA framework and instance-level scenarios are [part of GitLab Rails] so that
+they're always in-sync with the codebase (especially the views).
-The actual test scenarios and steps are [part of GitLab Rails] so that they're
-always in-sync with the codebase.
+Note that:
-### Smoke tests
-
-Smoke tests are quick tests that may be run at any time (especially after the pre-deployment migrations).
+- knowledge of the internals of the application are not required
+- data needed for the tests can only be created using the GUI or the API
+- expectations can only be made against the browser page and API responses
-Much like feature tests - these tests run against the UI and ensure that basic functionality is working.
+Every new feature should come with a [test plan].
-> See [Smoke Tests](smoke.md) for more information.
+| Tests path | Testing engine | Notes |
+| ---------- | -------------- | ----- |
+| `qa/qa/specs/features/` | [Capybara] + [RSpec] + Custom QA framework | Tests should be placed under their corresponding [Product category] |
-Read a separate document about [end-to-end tests](end_to_end_tests.md) to
-learn more.
+> See [end-to-end tests](end_to_end_tests.md) for more information.
[multiple pieces]: ../architecture.md#components
[GitLab Shell]: https://gitlab.com/gitlab-org/gitlab-shell
@@ -138,8 +166,29 @@ learn more.
[GitLab Pages]: https://gitlab.com/gitlab-org/gitlab-pages
[GitLab Runner]: https://gitlab.com/gitlab-org/gitlab-runner
[GitLab Omnibus]: https://gitlab.com/gitlab-org/omnibus-gitlab
-[GitLab QA]: https://gitlab.com/gitlab-org/gitlab-qa
[part of GitLab Rails]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa
+[test plan]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/.gitlab/issue_templates/Test%20plan.md
+[Product category]: https://about.gitlab.com/handbook/product/categories/
+
+### Smoke tests
+
+Smoke tests are quick tests that may be run at any time (especially after the
+pre-deployment migrations).
+
+These tests run against the UI and ensure that basic functionality is working.
+
+> See [Smoke Tests](smoke.md) for more information.
+
+### GitLab QA orchestrator
+
+[GitLab QA orchestrator] is a tool that allows to test that all these pieces
+integrate well together by building a Docker image for a given version of GitLab
+Rails and running end-to-end tests (i.e. using Capybara) against it.
+
+Learn more in the [GitLab QA orchestrator README][gitlab-qa-readme].
+
+[GitLab QA orchestrator]: https://gitlab.com/gitlab-org/gitlab-qa
+[gitlab-qa-readme]: https://gitlab.com/gitlab-org/gitlab-qa/tree/master/README.md
## EE-specific tests
diff --git a/doc/development/ui_guide.md b/doc/development/ui_guide.md
index df6ac452300..dd206bb2ae9 100644
--- a/doc/development/ui_guide.md
+++ b/doc/development/ui_guide.md
@@ -54,12 +54,12 @@ information from database or file system
When exporting SVGs, be sure to follow the following guidelines:
-1. Convert all strokes to outlines.
-2. Use pathfinder tools to combine overlapping paths and create compound paths.
-3. SVGs that are limited to one color should be exported without a fill color so the color can be set using CSS.
-4. Ensure that exported SVGs have been run through an [SVG cleaner](https://github.com/RazrFalcon/SVGCleaner) to remove unused elements and attributes.
+- Convert all strokes to outlines.
+- Use pathfinder tools to combine overlapping paths and create compound paths.
+- SVGs that are limited to one color should be exported without a fill color so the color can be set using CSS.
+- Ensure that exported SVGs have been run through an [SVG cleaner](https://github.com/RazrFalcon/SVGCleaner) to remove unused elements and attributes.
-You can open your svg in a text editor to ensure that it is clean.
+You can open your svg in a text editor to ensure that it is clean.
Incorrect files will look like this:
```xml
diff --git a/doc/development/utilities.md b/doc/development/utilities.md
index 0d074a3ef05..e5466ae8914 100644
--- a/doc/development/utilities.md
+++ b/doc/development/utilities.md
@@ -171,8 +171,8 @@ class Commit
extend Gitlab::Cache::RequestCache
def author
- User.find_by_any_email(author_email.downcase)
+ User.find_by_any_email(author_email)
end
- request_cache(:author) { author_email.downcase }
+ request_cache(:author) { author_email }
end
```
diff --git a/doc/development/ux_guide/tips.md b/doc/development/ux_guide/tips.md
index 8348de4f8a2..ceb9ed56ac4 100644
--- a/doc/development/ux_guide/tips.md
+++ b/doc/development/ux_guide/tips.md
@@ -1,20 +1,16 @@
# Tips
-## Contents
-* [SVGs](#svgs)
-
----
-
## SVGs
When exporting SVGs, be sure to follow the following guidelines:
1. Convert all strokes to outlines.
-2. Use pathfinder tools to combine overlapping paths and create compound paths.
-3. SVGs that are limited to one color should be exported without a fill color so the color can be set using CSS.
-4. Ensure that exported SVGs have been run through an [SVG cleaner](https://github.com/RazrFalcon/SVGCleaner) to remove unused elements and attributes.
+1. Use pathfinder tools to combine overlapping paths and create compound paths.
+1. SVGs that are limited to one color should be exported without a fill color so the color can be set using CSS.
+1. Ensure that exported SVGs have been run through an [SVG cleaner](https://github.com/RazrFalcon/SVGCleaner) to remove unused elements and attributes.
+
+You can open your SVG in a text editor to ensure that it is clean.
-You can open your svg in a text editor to ensure that it is clean.
Incorrect files will look like this:
```xml
@@ -35,10 +31,10 @@ Incorrect files will look like this:
</svg>
```
-Correct file will look like this:
+Correct files will look like this:
```xml
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 17" enable-background="new 0 0 16 17"><path d="m15.1 1h-2.1v-1h-2v1h-6v-1h-2v1h-2.1c-.5 0-.9.5-.9 1v14c0 .6.4 1 .9 1h14.2c.5 0 .9-.4.9-1v-14c0-.5-.4-1-.9-1m-1.1 14h-12v-9h12v9m0-11h-12v-1h12v1"/><path d="m5.4 11.6l1.5 1.2c.4.3 1.1.3 1.4-.1l2.5-3c.3-.4.3-1.1-.1-1.4-.5-.4-1.1-.3-1.5.1l-1.8 2.2-.8-.6c-.4-.3-1.1-.3-1.4.2-.3.4-.3 1 .2 1.4"/></svg>
```
-> TODO: Checkout [https://github.com/svg/svgo](https://github.com/svg/svgo)
+> TODO: Checkout <https://github.com/svg/svgo>.
diff --git a/doc/development/ux_guide/users.md b/doc/development/ux_guide/users.md
index f9c395b2dff..30386e728c4 100644
--- a/doc/development/ux_guide/users.md
+++ b/doc/development/ux_guide/users.md
@@ -101,7 +101,7 @@ GitLab's interface initially attracted Nazim when he was comparing version contr
### Demographics
**Age**
-
+
42 years old
**Location**
@@ -148,11 +148,11 @@ Matthieu describes GitLab as:
>"the only tool that offers the real feeling of having everything you need in one place."
-He credits himself as being entirely responsible for moving his company to GitLab.
+He credits himself as being entirely responsible for moving his company to GitLab.
### Frustrations
#### Updating to the latest release
-Matthieu introduced his company to GitLab. He is responsible for maintaining and managing the company's installation in addition to his day job. He feels updates are too frequent and he doesn't always have sufficient time to update GitLab. As a result, he's not up to date with releases.
+Matthieu introduced his company to GitLab. He is responsible for maintaining and managing the company's installation in addition to his day job. He feels updates are too frequent and he doesn't always have sufficient time to update GitLab. As a result, he's not up to date with releases.
Matthieu tried to set up automatic updates, however, as he isn't a Systems Administrator, he wasn't confident in his setup. He feels he should be able to "upgrade without users even noticing" but hasn't figured out how to do this yet. Matthieu would like the "update process to be triggered from the Admin Panel, perhaps accompanied with a changelog and the option to skip updates."
@@ -173,11 +173,11 @@ It's Matthieu's responsibility to get teams across his organization up and runni
He states that there has been: "a sluggishness of others to adapt" and it's "a low-effort adaptation at that."
### Goals
-* To save time. One of the reasons Matthieu moved his company to GitLab was to reduce the effort it took him to manage and configure multiple tools, thus saving him time. He has to balance his day job in addition to managing the company's GitLab installation and onboarding new teams to GitLab.
-* To use a platform which is easy to manage. Matthieu isn't a Systems Administrator, and when updating GitLab, creating backups, etc. He would prefer to work within GitLab's UI. Explanations / guided instructions when configuring settings in GitLab's interface would really help Matthieu. He needs reassurance that what he is about to change is
+* To save time. One of the reasons Matthieu moved his company to GitLab was to reduce the effort it took him to manage and configure multiple tools, thus saving him time. He has to balance his day job in addition to managing the company's GitLab installation and onboarding new teams to GitLab.
+* To use a platform which is easy to manage. Matthieu isn't a Systems Administrator, and when updating GitLab, creating backups, etc. He would prefer to work within GitLab's UI. Explanations / guided instructions when configuring settings in GitLab's interface would really help Matthieu. He needs reassurance that what he is about to change is
-1. the right setting
-2. will provide him with the desired result he wants.
+- The right setting.
+- Will provide him with the desired result he wants.
* Matthieu needs to educate his colleagues about GitLab. Matthieu's colleagues won't adopt GitLab as they're unaware of its capabilities and the positive impact it could have on their work. Matthieu needs support in getting this message across to them.
@@ -307,4 +307,4 @@ Karolina has an interest in UX and therefore has strong opinions about how GitLa
### Goals
* To develop her programming experience and to learn from other developers.
* To contribute to both her own and other open source projects.
-* To use a fast and intuitive version control platform. \ No newline at end of file
+* To use a fast and intuitive version control platform.
diff --git a/doc/development/what_requires_downtime.md b/doc/development/what_requires_downtime.md
index b668c9de6a0..3630a28fae9 100644
--- a/doc/development/what_requires_downtime.md
+++ b/doc/development/what_requires_downtime.md
@@ -300,7 +300,7 @@ The same applies to `rename_column_using_background_migration`:
1. Create a migration using the helper, which will schedule background
migrations to spread the writes over a longer period of time.
-2. In the next monthly release, create a clean-up migration to steal from the
+1. In the next monthly release, create a clean-up migration to steal from the
Sidekiq queues, migrate any missing rows, and cleanup the rename. This
migration should skip the steps after stealing from the Sidekiq queues if the
column has already been renamed.
diff --git a/doc/gitlab-basics/create-your-ssh-keys.md b/doc/gitlab-basics/create-your-ssh-keys.md
index b6ebe374de3..881629c3bfd 100644
--- a/doc/gitlab-basics/create-your-ssh-keys.md
+++ b/doc/gitlab-basics/create-your-ssh-keys.md
@@ -12,7 +12,7 @@
![SSH Keys](img/profile_settings_ssh_keys.png)
-3. Paste your **public** key that you generated in the first step in the 'Key'
+1. Paste your **public** key that you generated in the first step in the 'Key'
box.
![Paste SSH public key](img/profile_settings_ssh_keys_paste_pub.png)
diff --git a/doc/install/README.md b/doc/install/README.md
index 27df03c6ac6..92116305775 100644
--- a/doc/install/README.md
+++ b/doc/install/README.md
@@ -34,11 +34,11 @@ the hardware requirements.
- [Install GitLab on Google Cloud Platform](google_cloud_platform/index.md)
- [Install GitLab on Google Kubernetes Engine (GKE)](https://about.gitlab.com/2017/01/23/video-tutorial-idea-to-production-on-google-container-engine-gke/): video tutorial on
the full process of installing GitLab on Google Kubernetes Engine (GKE), pushing an application to GitLab, building the app with GitLab CI/CD, and deploying to production.
-- [Install on AWS](https://about.gitlab.com/aws/)
-- _Testing only!_ [DigitalOcean and Docker Machine](digitaloceandocker.md) -
- Quickly test any version of GitLab on DigitalOcean using Docker Machine.
+- [Install on AWS](aws/index.md): Install GitLab on AWS using the community AMIs that GitLab provides.
- [Getting started with GitLab and DigitalOcean](https://about.gitlab.com/2016/04/27/getting-started-with-gitlab-and-digitalocean/): requirements, installation process, updates.
- [Demo: Cloud Native Development with GitLab](https://about.gitlab.com/2017/04/18/cloud-native-demo/): video demonstration on how to install GitLab on Kubernetes, build a project, create Review Apps, store Docker images in Container Registry, deploy to production on Kubernetes, and monitor with Prometheus.
+- _Testing only!_ [DigitalOcean and Docker Machine](digitaloceandocker.md) -
+ Quickly test any version of GitLab on DigitalOcean using Docker Machine.
## Database
diff --git a/doc/install/aws/img/add_tags.png b/doc/install/aws/img/add_tags.png
new file mode 100644
index 00000000000..3572cd5daa1
--- /dev/null
+++ b/doc/install/aws/img/add_tags.png
Binary files differ
diff --git a/doc/install/aws/img/associate_subnet_gateway.png b/doc/install/aws/img/associate_subnet_gateway.png
new file mode 100644
index 00000000000..1edca974fca
--- /dev/null
+++ b/doc/install/aws/img/associate_subnet_gateway.png
Binary files differ
diff --git a/doc/install/aws/img/associate_subnet_gateway_2.png b/doc/install/aws/img/associate_subnet_gateway_2.png
new file mode 100644
index 00000000000..76e101d32a3
--- /dev/null
+++ b/doc/install/aws/img/associate_subnet_gateway_2.png
Binary files differ
diff --git a/doc/install/aws/img/aws_diagram.png b/doc/install/aws/img/aws_diagram.png
new file mode 100644
index 00000000000..bcd5c69bbeb
--- /dev/null
+++ b/doc/install/aws/img/aws_diagram.png
Binary files differ
diff --git a/doc/install/aws/img/choose_ami.png b/doc/install/aws/img/choose_ami.png
new file mode 100644
index 00000000000..034ac92691d
--- /dev/null
+++ b/doc/install/aws/img/choose_ami.png
Binary files differ
diff --git a/doc/install/aws/img/create_gateway.png b/doc/install/aws/img/create_gateway.png
new file mode 100644
index 00000000000..9408520e050
--- /dev/null
+++ b/doc/install/aws/img/create_gateway.png
Binary files differ
diff --git a/doc/install/aws/img/create_route_table.png b/doc/install/aws/img/create_route_table.png
new file mode 100644
index 00000000000..ea72c57257e
--- /dev/null
+++ b/doc/install/aws/img/create_route_table.png
Binary files differ
diff --git a/doc/install/aws/img/create_security_group.png b/doc/install/aws/img/create_security_group.png
new file mode 100644
index 00000000000..9a0dfccfe37
--- /dev/null
+++ b/doc/install/aws/img/create_security_group.png
Binary files differ
diff --git a/doc/install/aws/img/create_subnet.png b/doc/install/aws/img/create_subnet.png
new file mode 100644
index 00000000000..26f5ab1b625
--- /dev/null
+++ b/doc/install/aws/img/create_subnet.png
Binary files differ
diff --git a/doc/install/aws/img/create_vpc.png b/doc/install/aws/img/create_vpc.png
new file mode 100644
index 00000000000..a678f7013fd
--- /dev/null
+++ b/doc/install/aws/img/create_vpc.png
Binary files differ
diff --git a/doc/install/aws/img/ec_az.png b/doc/install/aws/img/ec_az.png
new file mode 100644
index 00000000000..22a8291c593
--- /dev/null
+++ b/doc/install/aws/img/ec_az.png
Binary files differ
diff --git a/doc/install/aws/img/ec_subnet.png b/doc/install/aws/img/ec_subnet.png
new file mode 100644
index 00000000000..c44fb4485e3
--- /dev/null
+++ b/doc/install/aws/img/ec_subnet.png
Binary files differ
diff --git a/doc/install/aws/img/policies.png b/doc/install/aws/img/policies.png
new file mode 100644
index 00000000000..e99497a52a2
--- /dev/null
+++ b/doc/install/aws/img/policies.png
Binary files differ
diff --git a/doc/install/aws/img/rds_subnet_group.png b/doc/install/aws/img/rds_subnet_group.png
new file mode 100644
index 00000000000..7c6157e38e0
--- /dev/null
+++ b/doc/install/aws/img/rds_subnet_group.png
Binary files differ
diff --git a/doc/install/aws/index.md b/doc/install/aws/index.md
new file mode 100644
index 00000000000..53fe1a6b25b
--- /dev/null
+++ b/doc/install/aws/index.md
@@ -0,0 +1,655 @@
+# Installing GitLab on Amazon Web Services (AWS)
+
+To install GitLab on AWS, you can use the Amazon Machine Images (AMIs) that GitLab
+provides with [each release](https://about.gitlab.com/releases/).
+
+This page offers a walkthrough of a common HA (Highly Available) configuration
+for GitLab on AWS. You should customize it to accommodate your needs.
+
+## Introduction
+
+GitLab on AWS can leverage many of the services that are already
+configurable with GitLab High Availability (HA). These services offer a great deal of
+flexibility and can be adapted to the needs of most companies, while enabling the
+automation of both vertical and horizontal scaling.
+
+In this guide, we'll go through a basic HA setup where we'll start by
+configuring our Virtual Private Cloud and subnets to later integrate
+services such as RDS for our database server and ElastiCache as a Redis
+cluster to finally manage them within an auto scaling group with custom
+scaling policies.
+
+## Requirements
+
+In addition to having a basic familiarity with [AWS](https://docs.aws.amazon.com/) and [Amazon EC2](https://docs.aws.amazon.com/ec2/), you will need:
+
+- [An AWS account](https://console.aws.amazon.com/console/home)
+- [To create or upload an SSH key](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html)
+ to connect to the instance via SSH
+- A domain name for the GitLab instance
+
+## Architecture
+
+Below is a diagram of the recommended architecture.
+
+![AWS architecture diagram](img/aws_diagram.png)
+
+## AWS costs
+
+Here's a list of the AWS services we will use, with links to pricing information:
+
+- **EC2**: GitLab will deployed on shared hardware which means
+ [on-demand pricing](https://aws.amazon.com/ec2/pricing/on-demand)
+ will apply. If you want to run it on a dedicated or reserved instance,
+ consult the [EC2 pricing page](https://aws.amazon.com/ec2/pricing/) for more
+ information on the cost.
+- **EBS**: We will also use an EBS volume to store the Git data. See the
+ [Amazon EBS pricing](https://aws.amazon.com/ebs/pricing/).
+- **S3**: We will use S3 to store backups, artifacts, LFS objects, etc. See the
+ [Amazon S3 pricing](https://aws.amazon.com/s3/pricing/).
+- **ALB**: An Application Load Balancer will be used to route requests to the
+ GitLab instance. See the [Amazon ELB pricing](https://aws.amazon.com/elasticloadbalancing/pricing/).
+- **RDS**: An Amazon Relational Database Service using PostgreSQL will be used
+ to provide a High Availability database configuration. See the
+ [Amazon RDS pricing](https://aws.amazon.com/rds/postgresql/pricing/).
+- **ElastiCache**: An in-memory cache environment will be used to provide a
+ High Availability Redis configuration. See the
+ [Amazon ElastiCache pricing](https://aws.amazon.com/elasticache/pricing/).
+
+## Creating an IAM EC2 instance role and profile
+To minimize the permissions of the user, we'll create a new [IAM](https://docs.aws.amazon.com/IAM/latest/UserGuide/introduction.html)
+role with limited access:
+
+1. Navigate to the IAM dashboard https://console.aws.amazon.com/iam/home and
+ click **Create role**.
+1. Create a new role by selecting **AWS service > EC2**, then click
+ **Next: Permissions**.
+1. Choose **AmazonEC2FullAccess** and **AmazonS3FullAccess**, then click **Next: Review**.
+1. Give the role the name `GitLabAdmin` and click **Create role**.
+
+## Configuring the network
+
+We'll start by creating a VPC for our GitLab cloud infrastructure, then
+we can create subnets to have public and private instances in at least
+two [Availability Zones (AZs)](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html). Public subnets will require a Route Table keep and an associated
+Internet Gateway.
+
+### Creating the Virtual Private Cloud (VPC)
+
+We'll now create a VPC, a virtual networking environment that you'll control:
+
+1. Navigate to https://console.aws.amazon.com/vpc/home.
+1. Select **Your VPCs** from the left menu and then click **Create VPC**.
+ At the "Name tag" enter `gitlab-vpc` and at the "IPv4 CIDR block" enter
+ `10.0.0.0/16`. If you don't require dedicated hardware, you can leave
+ "Tenancy" as default. Click **Yes, Create** when ready.
+
+ ![Create VPC](img/create_vpc.png)
+
+### Subnets
+
+Now, let's create some subnets in different Availability Zones. Make sure
+that each subnet is associated the the VPC we just created and
+that CIDR blocks don't overlap. This will also
+allow us to enable multi AZ for redundancy.
+
+We will create private and public subnets to match load balancers and
+RDS instances as well:
+
+1. Select **Subnets** from the left menu.
+1. Click **Create subnet**. Give it a descriptive name tag based on the IP,
+ for example `gitlab-public-10.0.0.0`, select the VPC we created previously,
+ and at the IPv4 CIDR block let's give it a 24 subnet `10.0.0.0/24`:
+
+ ![Create subnet](img/create_subnet.png)
+
+1. Follow the same steps to create all subnets:
+
+ | Name tag | Type |Availability Zone | CIDR block |
+ | -------- | ---- | ---------------- | ---------- |
+ | gitlab-public-10.0.0.0 | public | us-west-2a | 10.0.0.0 |
+ | gitlab-private-10.0.1.0 | private | us-west-2a | 10.0.1.0 |
+ | gitlab-public-10.0.2.0 | public | us-west-2b | 10.0.2.0 |
+ | gitlab-private-10.0.3.0 | private | us-west-2b | 10.0.3.0 |
+
+### Route Table
+
+Up to now all our subnets are private. We need to create a Route Table
+to associate an Internet Gateway. On the same VPC dashboard:
+
+1. Select **Route Tables** from the left menu.
+1. Click **Create Route Table**.
+1. At the "Name tag" enter `gitlab-public` and choose `gitlab-vpc` under "VPC".
+1. Hit **Yes, Create**.
+
+### Internet Gateway
+
+Now, still on the same dashboard, go to Internet Gateways and
+create a new one:
+
+1. Select **Internet Gateways** from the left menu.
+1. Click **Create internet gateway**, give it the name `gitlab-gateway` and
+ click **Create**.
+1. Select it from the table, and then under the **Actions** dropdown choose
+ "Attach to VPC".
+
+ ![Create gateway](img/create_gateway.png)
+
+1. Choose `gitlab-vpc` from the list and hit **Attach**.
+
+### Configuring subnets
+
+We now need to add a new target which will be our Internet Gateway and have
+it receive traffic from any destination.
+
+1. Select **Route Tables** from the left menu and select the `gitlab-public`
+ route to show the options at the bottom.
+1. Select the **Routes** tab, hit **Edit > Add another route** and set `0.0.0.0/0`
+ as destination. In the target, select the `gitlab-gateway` we created previously.
+ Hit **Save** once done.
+
+ ![Associate subnet with gateway](img/associate_subnet_gateway.png)
+
+Next, we must associate the **public** subnets to the route table:
+
+1. Select the **Subnet Associations** tab and hit **Edit**.
+1. Check only the public subnet and hit **Save**.
+
+ ![Associate subnet with gateway](img/associate_subnet_gateway_2.png)
+
+---
+
+Now that we're done with the network, let's create a security group.
+
+## Creating a security group
+
+The security group is basically the firewall:
+
+1. Select **Security Groups** from the left menu.
+1. Click **Create Security Group** and fill in the details. Give it a name,
+ add a description, and choose the VPC we created previously
+1. Select the security group from the list and at the the bottom select the
+ Inbound Rules tab. You will need to open the SSH, HTTP, and HTTPS ports. Set
+ the source to `0.0.0.0/0`.
+
+ ![Create security group](img/create_security_group.png)
+
+ TIP: **Tip:**
+ Based on best practices, you should allow SSH traffic from only a known
+ host or CIDR block. In that case, change the SSH source to be custom and give
+ it the IP you want to SSH from.
+
+1. When done, click **Save**.
+
+## PostgreSQL with RDS
+
+For our database server we will use Amazon RDS which offers Multi AZ
+for redundancy. Let's start by creating a subnet group and then we'll
+create the actual RDS instance.
+
+### RDS Subnet Group
+
+1. Navigate to the RDS dashboard and select **Subnet Groups** from the left menu.
+1. Give it a name (`gitlab-rds-group`), a description, and choose the VPC from
+ the VPC dropdown.
+1. Click "Add all the subnets related to this VPC" and
+ remove the public ones, we only want the **private subnets**.
+ In the end, you should see `10.0.1.0/24` and `10.0.3.0/24` (as
+ we defined them in the [subnets section](#subnets)).
+ Click **Create** when ready.
+
+ ![RDS Subnet Group](img/rds_subnet_group.png)
+
+### Creating the database
+
+Now, it's time to create the database:
+
+1. Select **Instances** from the left menu and click **Create database**.
+1. Select PostgreSQL and click **Next**.
+1. Since this is a production server, let's choose "Production". Click **Next**.
+1. Let's see the instance specifications:
+ 1. Leave the license model as is (`postgresql-license`).
+ 1. For the version, select the latest of the 9.6 series (check the
+ [database requirements](../../install/requirements.md#postgresql-requirements))
+ if there are any updates on this).
+ 1. For the size, let's select a `t2.medium` instance.
+ 1. Multi-AZ-deployment is recommended as redundancy, so choose "Create
+ replica in different zone". Read more at
+ [High Availability (Multi-AZ)](http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.MultiAZ.html).
+ 1. A Provisioned IOPS (SSD) storage type is best suited for HA (though you can
+ choose a General Purpose (SSD) to reduce the costs). Read more about it at
+ [Storage for Amazon RDS](http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html).
+
+1. The rest of the settings on this page request a DB isntance identifier, username
+ and a master password. We've chosen to use `gitlab-db-ha`, `gitlab` and a
+ very secure password respectively. Keep these in hand for later.
+1. Click **Next** to proceed to the advanced settings.
+1. Make sure to choose our gitlab VPC, our subnet group, set public accessibility to
+ **No**, and to leave it to create a new security group. The only additional
+ change which will be helpful is the database name for which we can use
+ `gitlabhq_production`. At the very bottom, there's an option to enable
+ auto updates to minor versions. You may want to turn it off.
+1. When done, click **Create database**.
+
+### Installing the `pg_trgm` extension for PostgreSQL
+
+Once the database is created, connect to your new RDS instance to verify access
+and to install a required extension.
+
+You can find the host or endpoint by selecting the instance you just created and
+after the details drop down you'll find it labeled as 'Endpoint'. Do not to
+include the colon and port number:
+
+```sh
+sudo /opt/gitlab/embedded/bin/psql -U gitlab -h <rds-endpoint> -d gitlabhq_production
+```
+
+At the psql prompt create the extension and then quit the session:
+
+```sh
+psql (9.4.7)
+Type "help" for help.
+
+gitlab=# CREATE EXTENSION pg_trgm;
+gitlab=# \q
+```
+
+---
+
+Now that the database is created, let's move on setting up Redis with ElasticCache.
+
+## Redis with ElastiCache
+
+ElastiCache is an in-memory hosted caching solution. Redis maintains its own
+persistence and is used for certain types of the GitLab application.
+
+To set up Redis:
+
+1. Navigate to the ElastiCache dashboard from your AWS console.
+1. Go to **Subnet Groups** in the left menu, and create a new subnet group.
+ Make sure to select our VPC and its [private subnets](#subnets). Click
+ **Create** when ready.
+
+ ![ElastiCache subnet](img/ec_subnet.png)
+
+1. Select **Redis** on the left menu and click **Create** to create a new
+ Redis cluster. Depending on your load, you can choose whether to enable
+ cluster mode or not. Even without cluster mode on, you still get the
+ chance to deploy Redis in multi availability zones. In this guide, we chose
+ not to enable it.
+1. In the settings section:
+ 1. Give the cluster a name (`gitlab-redis`) and a description.
+ 1. For the version, select the latest of `3.2` series (e.g., `3.2.10`).
+ 1. Select the node type and the number of replicas.
+1. In the advanced settings section:
+ 1. Select the multi-AZ auto-failover option.
+ 1. Select the subnet group we created previously.
+ 1. Manually select the preferred availability zones, and under "Replica 2"
+ choose a different zone than the other two.
+
+ ![Redis availability zones](img/ec_az.png)
+
+1. In the security settings, edit the security groups and choose the
+ `gitlab-security-group` we had previously created.
+1. Leave the rest of the settings to their default values or edit to your liking.
+1. When done, click **Create**.
+
+## RDS and Redis Security Group
+
+Let's navigate to our EC2 security groups and add a small change for our EC2
+instances to be able to connect to RDS. First, copy the security group name we
+defined, namely `gitlab-security-group`, select the RDS security group and edit the
+inbound rules. Choose the rule type to be PostgreSQL and paste the name under
+source.
+
+Similar to the above, jump to the `gitlab-security-group` group
+and add a custom TCP rule for port `6379` accessible within itself.
+
+## Load Balancer
+
+On the EC2 dashboard, look for Load Balancer on the left column:
+
+1. Click the **Create Load Balancer** button.
+ 1. Choose the Application Load Balancer.
+ 1. Give it a name (`gitlab-loadbalancer`) and set the scheme to "internet-facing".
+ 1. In the "Listeners" section, make sure it has HTTP and HTTPS.
+ 1. In the "Availability Zones" section, select the `gitlab-vpc` we have created
+ and associate the **public subnets**.
+1. Click **Configure Security Settings** to go to the next section to
+ select the TLS certificate. When done, go to the next step.
+1. In the "Security Groups" section, create a new one by giving it a name
+ (`gitlab-loadbalancer-sec-group`) and allow both HTTP ad HTTPS traffic
+ from anywhere (`0.0.0.0/0, ::/0`).
+1. In the next step, configure the routing and select an existing target group
+ (`gitlab-public`). The Load Balancer Health will allow us to indicate where to
+ ping and what makes up a healthy or unhealthy instance.
+1. Leave the "Register Targets" section as is, and finally review the settings
+ and create the ELB.
+
+After the Load Balancer is up and running, you can revisit your Security
+Groups to refine the access only through the ELB and any other requirement
+you might have.
+
+## Deploying GitLab inside an auto scaling group
+
+We'll use AWS's wizard to deploy GitLab and then SSH into the instance to
+configure the PostgreSQL and Redis connections.
+
+The Auto Scaling Group option is available through the EC2 dashboard on the left
+sidebar.
+
+1. Click **Create Auto Scaling group**.
+1. Create a new launch configuration.
+
+### Choose the AMI
+
+Choose the AMI:
+
+1. Go to the Community AMIs and search for `GitLab EE <version>`
+ where `<version>` the latest version as seen on the
+ [releases page](https://about.gitlab.com/releases/).
+
+ ![Choose AMI](img/choose_ami.png)
+
+### Choose an instance type
+
+You should choose an instance type based on your workload. Consult
+[the hardware requirements](../requirements.md#hardware-requirements) to choose
+one that fits your needs (at least `c4.xlarge`, which is enough to accommodate 100 users):
+
+1. Choose the your instance type.
+1. Click **Next: Configure Instance Details**.
+
+### Configure details
+
+In this step we'll configure some details:
+
+1. Enter a name (`gitlab-autoscaling`).
+1. Select the IAM role we created.
+1. Optionally, enable CloudWatch and the EBS-optimized instance settings.
+1. In the "Advanced Details" section, set the IP address type to
+ "Do not assign a public IP address to any instances."
+1. Click **Next: Add Storage**.
+
+### Add storage
+
+The root volume is 8GB by default and should be enough given that we won't store
+any data there. Let's create a new EBS volume that will host the Git data. Its
+size depends on your needs and you can always migrate to a bigger volume later.
+You will be able to [set up that volume](#setting-up-the-ebs-volume)
+after the instance is created.
+
+### Configure security group
+
+As a last step, configure the security group:
+
+1. Select the existing load balancer security group we have [created](#load-balancer).
+1. Select **Review**.
+
+### Review and launch
+
+Now is a good time to review all the previous settings. When ready, click
+**Create launch configuration** and select the SSH key pair with which you will
+connect to the instance.
+
+### Create Auto Scaling Group
+
+We are now able to start creating our Auto Scaling Group:
+
+1. Give it a group name.
+1. Set the group size to 2 as we want to always start with two instances.
+1. Assign it our network VPC and add the **private subnets**.
+1. In the "Advanced Details" section, choose to receive traffic from ELBs
+ and select our ELB.
+1. Choose the ELB health check.
+1. Click **Next: Configure scaling policies**.
+
+This is the really great part of Auto Scaling; we get to choose when AWS
+launches new instances and when it removes them. For this group we'll
+scale between 2 and 4 instances where one instance will be added if CPU
+utilization is greater than 60% and one instance is removed if it falls
+to less than 45%.
+
+![Auto scaling group policies](img/policies.png)
+
+Finally, configure notifications and tags as you see fit, and create the
+auto scaling group.
+
+You'll notice that after we save the configuration, AWS starts launching our two
+instances in different AZs and without a public IP which is exactly what
+we intended.
+
+## After deployment
+
+After a few minutes, the instances should be up and accessible via the internet.
+Let's connect to the primary and configure some things before logging in.
+
+### Configuring GitLab to connect with postgres and Redis
+
+While connected to your server, let's connect to the RDS instance to verify
+access and to install a required extension:
+
+```sh
+sudo /opt/gitlab/embedded/bin/psql -U gitlab -h <rds-endpoint> -d gitlabhq_production
+```
+
+Edit the `gitlab.rb` file at `/etc/gitlab/gitlab.rb`
+find the `external_url 'http://gitlab.example.com'` option and change it
+to the domain you will be using or the public IP address of the current
+instance to test the configuration.
+
+For a more detailed description about configuring GitLab, see [Configuring GitLab for HA](../../administration/high_availability/gitlab.md)
+
+Now look for the GitLab database settings and uncomment as necessary. In
+our current case we'll specify the database adapter, encoding, host, name,
+username, and password:
+
+```ruby
+# Disable the built-in Postgres
+postgresql['enable'] = false
+
+# Fill in the connection details
+gitlab_rails['db_adapter'] = "postgresql"
+gitlab_rails['db_encoding'] = "unicode"
+gitlab_rails['db_database'] = "gitlabhq_production"
+gitlab_rails['db_username'] = "gitlab"
+gitlab_rails['db_password'] = "mypassword"
+gitlab_rails['db_host'] = "<rds-endpoint>"
+```
+
+Next, we need to configure the Redis section by adding the host and
+uncommenting the port:
+
+```ruby
+# Disable the built-in Redis
+redis['enable'] = false
+
+# Fill in the connection details
+gitlab_rails['redis_host'] = "<redis-endpoint>"
+gitlab_rails['redis_port'] = 6379
+```
+
+Finally, reconfigure GitLab for the change to take effect:
+
+
+```sh
+sudo gitlab-ctl reconfigure
+```
+
+You might also find it useful to run a check and a service status to make sure
+everything has been setup correctly:
+
+```sh
+sudo gitlab-rake gitlab:check
+sudo gitlab-ctl status
+```
+
+If everything looks good, you should be able to reach GitLab in your browser.
+
+### Setting up the EBS volume
+
+The EBS volume will host the Git repositories data:
+
+1. First, format the `/dev/xvdb` volume and then mount it under the directory
+ where the data will be stored. For example, `/mnt/gitlab-data/`.
+1. Tell GitLab to store its data in the new directory by editing
+ `/etc/gitlab/gitlab.rb` with your editor:
+
+ ```ruby
+ git_data_dirs({
+ "default" => { "path" => "/mnt/gitlab-data" }
+ })
+ ```
+
+ where `/mnt/gitlab-data` the location where you will store the Git data.
+
+1. Save the file and reconfigure GitLab:
+
+ ```sh
+ sudo gitlab-ctl reconfigure
+ ```
+
+TIP: **Tip:**
+If you wish to add more than one data volumes to store the Git repositories,
+read the [repository storage paths docs](../../administration/repository_storage_paths.md).
+
+### Setting up Gitaly
+
+Gitaly is a service that provides high-level RPC access to Git repositories.
+It should be enabled and configured in a separate EC2 instance on the
+[private VPC](#subnets) we configured previously.
+
+Follow the [documentation to set up Gitaly](../../administration/gitaly/index.md).
+
+### Using Amazon S3 object storage
+
+GitLab stores many objects outside the Git repository, many of which can be
+uploaded to S3. That way, you can offload the root disk volume of these objects
+which would otherwise take much space.
+
+In particular, you can store in S3:
+
+- [The Git LFS objects](../../workflow/lfs/lfs_administration.md#s3-for-omnibus-installations) ((Omnibus GitLab installations))
+- [The Container Registry images](../../administration/container_registry.md#container-registry-storage-driver) (Omnibus GitLab installations)
+- [The GitLab CI/CD job artifacts](../../administration/job_artifacts.md#using-object-storage) (Omnibus GitLab installations)
+
+### Setting up a domain name
+
+After you SSH into the instance, configure the domain name:
+
+1. Open `/etc/gitlab/gitlab.rb` with your preferred editor.
+1. Edit the `external_url` value:
+
+ ```ruby
+ external_url 'http://example.com'
+ ```
+
+1. Reconfigure GitLab:
+
+ ```sh
+ sudo gitlab-ctl reconfigure
+ ```
+
+You should now be able to reach GitLab at the URL you defined. To use HTTPS
+(recommended), see the [HTTPS documentation](https://docs.gitlab.com/omnibus/settings/nginx.html#enable-https).
+
+### Logging in for the first time
+
+If you followed the previous section, you should be now able to visit GitLab
+in your browser. The very first time, you will be asked to set up a password
+for the `root` user which has admin privileges on the GitLab instance.
+
+After you set it up, login with username `root` and the newly created password.
+
+## Health check and monitoring with Prometheus
+
+Apart from Amazon's Cloudwatch which you can enable on various services,
+GitLab provides its own integrated monitoring solution based on Prometheus.
+For more information on how to set it up, visit the
+[GitLab Prometheus documentation](../../administration/monitoring/prometheus/index.md)
+
+GitLab also has various [health check endpoints](../..//user/admin_area/monitoring/health_check.md)
+that you can ping and get reports.
+
+## GitLab Runners
+
+If you want to take advantage of [GitLab CI/CD](../../ci/README.md), you have to
+set up at least one [GitLab Runner](https://docs.gitlab.com/runner/).
+
+Read more on configuring an
+[autoscaling GitLab Runner on AWS](https://docs.gitlab.com/runner/configuration/runner_autoscale_aws/).
+
+## Backup and restore
+
+GitLab provides [a tool to backup](../../raketasks/backup_restore.md#creating-a-backup-of-the-gitlab-system)
+and restore its Git data, database, attachments, LFS objects, etc.
+
+Some important things to know:
+
+- The backup/restore tool **does not** store some configuration files, like secrets; you'll
+ need to [configure this yourself](../../raketasks/backup_restore.md#storing-configuration-files).
+- By default, the backup files are stored locally, but you can
+ [backup GitLab using S3](../../raketasks/backup_restore.md#using-amazon-s3).
+- You can [exclude specific directories form the backup](../../raketasks/backup_restore.md#excluding-specific-directories-from-the-backup).
+
+### Backing up GitLab
+
+To back up GitLab:
+
+1. SSH into your instance.
+1. Take a backup:
+
+ ```sh
+ sudo gitlab-rake gitlab:backup:create
+ ```
+
+### Restoring GitLab from a backup
+
+To restore GitLab, first review the [restore documentation](../../raketasks/backup_restore.md#restore),
+and primarily the restore prerequisites. Then, follow the steps under the
+[Omnibus installations section](../../raketasks/backup_restore.md#restore-for-omnibus-installations).
+
+## Updating GitLab
+
+GitLab releases a new version every month on the 22nd. Whenever a new version is
+released, you can update your GitLab instance:
+
+1. SSH into your instance
+1. Take a backup:
+
+ ```sh
+ sudo gitlab-rake gitlab:backup:create
+ ```
+
+1. Update the repositories and install GitLab:
+
+ ```sh
+ sudo apt update
+ sudo apt install gitlab-ee
+ ```
+
+After a few minutes, the new version should be up and running.
+
+## Conclusion
+
+In this guide, we went mostly through scaling and some redundancy options,
+your mileage may vary.
+
+Keep in mind that all Highly Available solutions come with a trade-off between
+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.
+
+Have a read through these other resources and feel free to
+[open an issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/new)
+to request additional material:
+
+- [GitLab High Availability](https://docs.gitlab.com/ee/administration/high_availability/):
+ GitLab supports several different types of clustering and high-availability.
+- [Geo replication](https://docs.gitlab.com/ee/administration/geo/replication/):
+ Geo is the solution for widely distributed development teams.
+- [Omnibus GitLab](https://docs.gitlab.com/omnibus/) - Everything you need to know
+ about administering your GitLab instance.
+- [Upload a license](https://docs.gitlab.com/ee/user/admin_area/license.html):
+ Activate all GitLab Enterprise Edition functionality with a license.
+- [Pricing](https://about.gitlab.com/pricing): Pricing for the different tiers.
diff --git a/doc/install/database_mysql.md b/doc/install/database_mysql.md
index acaed53e052..4cb8ca4f3e7 100644
--- a/doc/install/database_mysql.md
+++ b/doc/install/database_mysql.md
@@ -1,15 +1,20 @@
# Database MySQL
-> **Note:**
-> - We do not recommend using MySQL due to various issues. For example, case
- [(in)sensitivity](https://dev.mysql.com/doc/refman/5.0/en/case-sensitivity.html)
- and [problems](https://bugs.mysql.com/bug.php?id=65830) that
- [suggested](https://bugs.mysql.com/bug.php?id=50909)
- [fixes](https://bugs.mysql.com/bug.php?id=65830) [have](https://bugs.mysql.com/bug.php?id=63164).
+NOTE: **Note:**
+We do not recommend using MySQL due to various issues.
+For example, there have been bugs with case
+[(in)sensitivity](https://dev.mysql.com/doc/refman/5.7/en/case-sensitivity.html).
+
+Bugs relating to case sensitivity:
+
+- <https://bugs.mysql.com/bug.php?id=65830>
+- <https://bugs.mysql.com/bug.php?id=50909>
+- <https://bugs.mysql.com/bug.php?id=65830>
+- <https://bugs.mysql.com/bug.php?id=63164>
## Initial database setup
-```
+```sh
# Install the database packages
sudo apt-get install -y mysql-server mysql-client libmysqlclient-dev
@@ -84,8 +89,9 @@ GitLab 8.14 has introduced [a feature](https://gitlab.com/gitlab-org/gitlab-ce/m
Follow the below instructions to ensure you use the most up to date requirements for your GitLab MySQL Database.
**We are about to do the following:**
+
- Ensure you can enable `utf8mb4` encoding and `utf8mb4_general_ci` collation for your GitLab DB, tables and data.
-- Convert your GitLab tables and data from `utf8`/`utf8_general_ci` to `utf8mb4`/`utf8mb4_general_ci`
+- Convert your GitLab tables and data from `utf8`/`utf8_general_ci` to `utf8mb4`/`utf8mb4_general_ci`.
### Check for utf8mb4 support
@@ -130,7 +136,8 @@ We need to check, enable and maybe convert your existing GitLab DB tables to the
Whatever the results of your checks above, we now need to check if your GitLab database has been created using [InnoDB File-Per-Table Tablespaces](http://dev.mysql.com/doc/refman/5.7/en/innodb-multiple-tablespaces.html) (i.e. `innodb_file_per_table` was set to **1** at initial setup time).
-> Note: This setting is [enabled by default](http://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_file_per_table) since MySQL 5.6.6.
+NOTE: **Note:**
+This setting is [enabled by default](http://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_file_per_table) since MySQL 5.6.6.
# Run this command with root privileges, replace the data dir if different:
sudo ls -lh /var/lib/mysql/gitlabhq_production/*.ibd | wc -l
@@ -138,20 +145,19 @@ Whatever the results of your checks above, we now need to check if your GitLab d
# Run this command with root privileges, replace the data dir if different:
sudo ls -lh /var/lib/mysql/gitlabhq_production/*.frm | wc -l
-
- **Case 1: a result > 0 for both commands**
-Congrats, your GitLab database uses the right InnoDB tablespace format.
+Congratulations, your GitLab database uses the right InnoDB tablespace format.
However, you must still ensure that any **future tables** created by GitLab will still use the right format:
- If `SELECT @@innodb_file_per_table` returned **1** previously, your server is running correctly.
- > It's however a requirement to check *now* that this setting is indeed persisted in your [my.cnf](https://dev.mysql.com/doc/refman/5.7/en/tablespace-enabling.html) file!
+ > It's however a requirement to check *now* that this setting is indeed persisted in your [`my.cnf`](https://dev.mysql.com/doc/refman/5.7/en/innodb-multiple-tablespaces.html) file!
- If `SELECT @@innodb_file_per_table` returned **0** previously, your server is not running correctly.
- > [Enable innodb_file_per_table](https://dev.mysql.com/doc/refman/5.7/en/tablespace-enabling.html) by running in a MySQL session as root the command `SET GLOBAL innodb_file_per_table=1, innodb_file_format=Barracuda;` and persist the two settings in your [my.cnf](https://dev.mysql.com/doc/refman/5.7/en/tablespace-enabling.html) file
+ > [Enable innodb_file_per_table](https://dev.mysql.com/doc/refman/5.7/en/innodb-multiple-tablespaces.html) by running in a MySQL session as root the command `SET GLOBAL innodb_file_per_table=1, innodb_file_format=Barracuda;` and persist the two settings in your [`my.cnf`](https://dev.mysql.com/doc/refman/5.7/en/innodb-multiple-tablespaces.html) file.
Now, if you have a **different result** returned by the 2 commands above, it means you have a **mix of tables format** uses in your GitLab database. This can happen if your MySQL server had different values for `innodb_file_per_table` in its life and you updated GitLab at different moments with those inconsistent values. So keep reading.
@@ -172,7 +178,7 @@ Let's enable what we need on the running server:
# You can now quit the database session
mysql> \q
-> Now, **persist** [innodb_file_per_table](https://dev.mysql.com/doc/refman/5.6/en/tablespace-enabling.html) and [innodb_file_format](https://dev.mysql.com/doc/refman/5.6/en/innodb-file-format-enabling.html) in your `my.cnf` file.
+> Now, **persist** [innodb_file_per_table](https://dev.mysql.com/doc/refman/5.7/en/innodb-multiple-tablespaces.html) and [innodb_file_format](https://dev.mysql.com/doc/refman/5.7/en/innodb-file-format-enabling.html) in your `my.cnf` file.
Ensure at this stage that your GitLab instance is indeed **stopped**.
@@ -184,7 +190,7 @@ Now, let's convert all the GitLab database tables to the new tablespace format:
# Type the MySQL root password
mysql > use gitlabhq_production;
- # Safety check: you should still have those values set as follow:
+ # Safety check: you should still have those values set as follows:
mysql> SELECT @@innodb_file_per_table, @@innodb_file_format;
+-------------------------+----------------------+
| @@innodb_file_per_table | @@innodb_file_format |
@@ -203,7 +209,7 @@ Now, let's convert all the GitLab database tables to the new tablespace format:
#### Check for proper InnoDB File Format, Row Format, Large Prefix and tables conversion
-We need to check, enable and probably convert your existing GitLab DB tables to use the [Barracuda InnoDB file format](https://dev.mysql.com/doc/refman/5.6/en/innodb-file-format.html), the [DYNAMIC row format](https://dev.mysql.com/doc/refman/5.6/en/glossary.html#glos_dynamic_row_format) and [innodb_large_prefix](http://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_large_prefix) as a second prerequisite for supporting **utfb8mb4 with long indexes** used by recent GitLab databases.
+We need to check, enable and probably convert your existing GitLab DB tables to use the [Barracuda InnoDB file format](https://dev.mysql.com/doc/refman/5.7/en/innodb-file-format.html), the [DYNAMIC row format](https://dev.mysql.com/doc/refman/5.7/en/glossary.html#glos_dynamic_row_format) and [innodb_large_prefix](http://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_large_prefix) as a second prerequisite for supporting **utfb8mb4 with long indexes** used by recent GitLab databases.
# Login to MySQL
mysql -u root -p
@@ -229,7 +235,7 @@ We need to check, enable and probably convert your existing GitLab DB tables to
| utf8 | utf8_general_ci |
+--------------------------+----------------------+
-> Now, ensure that [innodb_file_format](https://dev.mysql.com/doc/refman/5.6/en/tablespace-enabling.html) and [innodb_large_prefix](http://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_large_prefix) are **persisted** in your `my.cnf` file.
+> Now, ensure that [innodb_file_format](https://dev.mysql.com/doc/refman/5.7/en/innodb-multiple-tablespaces.html) and [innodb_large_prefix](http://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_large_prefix) are **persisted** in your `my.cnf` file.
#### Tables and data conversion to utf8mb4
@@ -257,7 +263,7 @@ Now that you have a persistent MySQL setup, you can safely upgrade tables after
Ensure your GitLab database configuration file uses a proper connection encoding and collation:
-```sudo -u git -H editor config/database.yml```
+`sudo -u git -H editor config/database.yml`
production:
adapter: mysql2
@@ -266,19 +272,19 @@ Ensure your GitLab database configuration file uses a proper connection encoding
[Restart your GitLab instance](../administration/restart_gitlab.md).
-
## MySQL strings limits
After installation or upgrade, remember to run the `add_limits_mysql` Rake task:
**Omnibus GitLab installations**
-```
+
+```sh
sudo gitlab-rake add_limits_mysql
```
**Installations from source**
-```
+```sh
bundle exec rake add_limits_mysql RAILS_ENV=production
```
diff --git a/doc/install/digitaloceandocker.md b/doc/install/digitaloceandocker.md
index 676392eacf2..d67695d75b4 100644
--- a/doc/install/digitaloceandocker.md
+++ b/doc/install/digitaloceandocker.md
@@ -1,7 +1,8 @@
# Digital Ocean and Docker Machine test environment
-## Warning. This guide is for quickly testing different versions of GitLab and
-## not recommended for ease of future upgrades or keeping the data you create.
+CAUTION: **Caution:**
+This guide is for quickly testing different versions of GitLab and not recommended for ease of
+future upgrades or keeping the data you create.
## Initial setup
@@ -12,92 +13,88 @@ locally on either macOS or Linux.
#### Install Docker Toolbox
-1. [https://www.docker.com/products/docker-toolbox](https://www.docker.com/products/docker-toolbox)
+- <https://www.docker.com/products/docker-toolbox>
### On Linux
#### Install Docker Engine
-1. [https://docs.docker.com/engine/installation/linux](https://docs.docker.com/engine/installation/linux/)
+- <https://docs.docker.com/engine/installation/linux/>
#### Install Docker Machine
-1. [https://docs.docker.com/machine/install-machine](https://docs.docker.com/machine/install-machine/)
+- <https://docs.docker.com/machine/install-machine/>
-_The rest of the steps are identical for macOS and Linux_
+NOTE: **Note:**
+The rest of the steps are identical for macOS and Linux.
### Create new docker host
-1. Login to Digital Ocean
-1. Generate a new API token at https://cloud.digitalocean.com/settings/api/tokens
+1. Login to Digital Ocean.
+1. Generate a new API token at <https://cloud.digitalocean.com/settings/api/tokens>.
+ This command will create a new DO droplet called `gitlab-test-env-do` that will act as a docker host.
-This command will create a new DO droplet called `gitlab-test-env-do` that will act as a docker host.
+ NOTE: **Note:**
+ 4GB is the minimum requirement for a Docker host that will run more than one GitLab instance.
-**Note: 4GB is the minimum requirement for a Docker host that will run more then one GitLab instance**
+ - RAM: 4GB
+ - Name: `gitlab-test-env-do`
+ - Driver: `digitalocean`
-+ RAM: 4GB
-+ Name: `gitlab-test-env-do`
-+ Driver: `digitalocean`
+1. Set the DO token:
+ ```sh
+ export DOTOKEN=<your generated token>
+ ```
-**Set the DO token** - Replace the string below with your generated token
+1. Create the machine:
-```
-export DOTOKEN=cf3dfd0662933203005c4a73396214b7879d70aabc6352573fe178d340a80248
-```
-
-**Create the machine**
-
-```
-docker-machine create \
- --driver digitalocean \
- --digitalocean-access-token=$DOTOKEN \
- --digitalocean-size "4gb" \
- gitlab-test-env-do
-```
-
-+ Resource: https://docs.docker.com/machine/drivers/digital-ocean/
+ ```sh
+ docker-machine create \
+ --driver digitalocean \
+ --digitalocean-access-token=$DOTOKEN \
+ --digitalocean-size "4gb" \
+ gitlab-test-env-do
+ ```
+Resource: <https://docs.docker.com/machine/drivers/digital-ocean/>.
### Creating GitLab test instance
-
#### Connect your shell to the new machine
-
In this example we'll create a GitLab EE 8.10.8 instance.
-
First connect the docker client to the docker host you created previously.
-```
+```sh
eval "$(docker-machine env gitlab-test-env-do)"
```
You can add this to your `~/.bash_profile` file to ensure the `docker` client uses the `gitlab-test-env-do` docker host
-
#### Create new GitLab container
-+ HTTP port: `8888`
-+ SSH port: `2222`
- + Set `gitlab_shell_ssh_port` using `--env GITLAB_OMNIBUS_CONFIG `
-+ Hostname: IP of docker host
-+ Container name: `gitlab-test-8.10`
-+ GitLab version: **EE** `8.10.8-ee.0`
+- HTTP port: `8888`
+- SSH port: `2222`
+ - Set `gitlab_shell_ssh_port` using `--env GITLAB_OMNIBUS_CONFIG`
+- Hostname: IP of docker host
+- Container name: `gitlab-test-8.10`
+- GitLab version: **EE** `8.10.8-ee.0`
-##### Set up container settings
+##### Set up container settings
-```
+```sh
export SSH_PORT=2222
export HTTP_PORT=8888
export VERSION=8.10.8-ee.0
export NAME=gitlab-test-8.10
```
-##### Create container
-```
+##### Create container
+
+```sh
docker run --detach \
--env GITLAB_OMNIBUS_CONFIG="external_url 'http://$(docker-machine ip gitlab-test-env-do):$HTTP_PORT'; gitlab_rails['gitlab_shell_ssh_port'] = $SSH_PORT;" \
--hostname $(docker-machine ip gitlab-test-env-do) \
@@ -110,23 +107,20 @@ gitlab/gitlab-ee:$VERSION
##### Retrieve the docker host IP
-```
+```sh
docker-machine ip gitlab-test-env-do
# example output: 192.168.151.134
```
-
-+ Browse to: http://192.168.151.134:8888/
-
+Browse to: <http://192.168.151.134:8888/>.
##### Execute interactive shell/edit configuration
-
-```
+```sh
docker exec -it $NAME /bin/bash
```
-```
+```sh
# example commands
root@192:/# vi /etc/gitlab/gitlab.rb
root@192:/# gitlab-ctl reconfigure
@@ -134,6 +128,6 @@ root@192:/# gitlab-ctl reconfigure
#### Resources
-+ [https://docs.gitlab.com/omnibus/docker/](https://docs.gitlab.com/omnibus/docker/)
-+ [https://docs.docker.com/machine/get-started/](https://docs.docker.com/machine/get-started/)
-+ [https://docs.docker.com/machine/reference/ip/](https://docs.docker.com/machine/reference/ip/)+
+- <https://docs.gitlab.com/omnibus/docker/>.
+- <https://docs.docker.com/machine/get-started/>.
+- <https://docs.docker.com/machine/reference/ip/>.
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 37c826ce9e0..cac97b63d92 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -12,7 +12,7 @@ Since installations from source don't have Runit, Sidekiq can't be terminated an
## Select Version to Install
-Make sure you view [this installation guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md) from the branch (version) of GitLab you would like to install (e.g., `11-4-stable`).
+Make sure you view [this installation guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md) from the branch (version) of GitLab you would like to install (e.g., `11-5-stable`).
You can select the branch in the version dropdown in the top left corner of GitLab (below the menu bar).
If the highest number stable branch is unclear please check the [GitLab Blog](https://about.gitlab.com/blog/) for installation guide links by version.
@@ -132,9 +132,9 @@ Remove the old Ruby 1.8 if present:
Download Ruby and compile it:
mkdir /tmp/ruby && cd /tmp/ruby
- curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.4/ruby-2.4.5.tar.gz
- echo '4d650f302f1ec00256450b112bb023644b6ab6dd ruby-2.4.5.tar.gz' | shasum -c - && tar xzf ruby-2.4.5.tar.gz
- cd ruby-2.4.5
+ curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.5/ruby-2.5.3.tar.gz
+ echo 'f919a9fbcdb7abecd887157b49833663c5c15fda ruby-2.5.3.tar.gz' | shasum -c - && tar xzf ruby-2.5.3.tar.gz
+ cd ruby-2.5.3
./configure --disable-install-rdoc
make
@@ -300,9 +300,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 11-4-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 11-5-stable gitlab
-**Note:** You can change `11-4-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `11-5-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
diff --git a/doc/install/kubernetes/gitlab_chart.md b/doc/install/kubernetes/gitlab_chart.md
index 6d1bc4aedc4..3f5b36f7254 100644
--- a/doc/install/kubernetes/gitlab_chart.md
+++ b/doc/install/kubernetes/gitlab_chart.md
@@ -31,7 +31,7 @@ to deploy.
TIP: **Tip:**
For production deployments, we strongly recommend using the
-[detailed installation instructions](https://gitlab.com/charts/gitlab/blob/master/doc/installation/README.md)
+[detailed installation instructions](https://gitlab.com/charts/gitlab/blob/master/doc/installation/index.md)
utilizing [external Postgres, Redis, and object storage](https://gitlab.com/charts/gitlab/tree/master/doc/advanced) services.
### Requirements
diff --git a/doc/install/kubernetes/gitlab_runner_chart.md b/doc/install/kubernetes/gitlab_runner_chart.md
index 2aab225fcdb..f34d398a7f5 100644
--- a/doc/install/kubernetes/gitlab_runner_chart.md
+++ b/doc/install/kubernetes/gitlab_runner_chart.md
@@ -1,6 +1,6 @@
# GitLab Runner Helm Chart
> **Note:**
-These charts have been tested on Google Kubernetes Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/gitlab-runner/issues).
+These charts have been tested on Google Kubernetes Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/gitlab-org/gitlab-runner/issues).
The `gitlab-runner` Helm chart deploys a GitLab Runner instance into your
Kubernetes cluster.
@@ -132,7 +132,7 @@ runners:
If your cluster has RBAC enabled, you can choose to either have the chart create its own service account or provide one.
-To have the chart create the service account for you, set `rbac.create` to true.
+To have the chart create the service account for you, set `rbac.create` to true.
### Controlling maximum Runner concurrency
diff --git a/doc/install/openshift_and_gitlab/index.md b/doc/install/openshift_and_gitlab/index.md
index 5c8a830ac8f..4c88b6f97fc 100644
--- a/doc/install/openshift_and_gitlab/index.md
+++ b/doc/install/openshift_and_gitlab/index.md
@@ -505,7 +505,7 @@ PaaS and managing your applications with the ease of containers.
[RedHat]: https://www.redhat.com/en "RedHat website"
[openshift]: https://www.openshift.org "OpenShift Origin website"
[vm]: https://www.openshift.org/vm/ "OpenShift All-in-one VM"
-[vm-new]: https://atlas.hashicorp.com/openshift/boxes/origin-all-in-one "Official OpenShift Vagrant box on Atlas"
+[vm-new]: https://app.vagrantup.com/openshift/boxes/origin-all-in-one "Official OpenShift Vagrant box on Vagrant Cloud"
[template]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/docker/openshift-template.json "OpenShift template for GitLab"
[openshift.com]: https://openshift.com "OpenShift Online"
[kubernetes]: http://kubernetes.io/ "Kubernetes website"
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 13a6a1c68ad..dcc6d75724e 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -36,7 +36,7 @@ Please consider using a virtual machine to run GitLab.
GitLab requires Ruby (MRI) 2.3. Support for Ruby versions below 2.3 (2.1, 2.2) will stop with GitLab 8.13.
You will have to use the standard MRI implementation of Ruby.
-We love [JRuby](http://jruby.org/) and [Rubinius](http://rubini.us/) but GitLab
+We love [JRuby](https://www.jruby.org/) and [Rubinius](https://rubinius.com) but GitLab
needs several Gems that have native extensions.
## Hardware requirements
diff --git a/doc/integration/akismet.md b/doc/integration/akismet.md
index a6436b5f926..200fe6f5206 100644
--- a/doc/integration/akismet.md
+++ b/doc/integration/akismet.md
@@ -20,17 +20,17 @@ To use Akismet:
1. Go to the URL: https://akismet.com/account/
-2. Sign-in or create a new account.
+1. Sign-in or create a new account.
-3. Click on **Show** to reveal the API key.
+1. Click on **Show** to reveal the API key.
-4. Go to Applications Settings on Admin Area (`admin/application_settings`)
+1. Go to Applications Settings on Admin Area (`admin/application_settings`)
-5. Check the **Enable Akismet** checkbox
+1. Check the **Enable Akismet** checkbox
-6. Fill in the API key from step 3.
+1. Fill in the API key from step 3.
-7. Save the configuration.
+1. Save the configuration.
![Screenshot of Akismet settings](img/akismet_settings.png)
@@ -42,9 +42,9 @@ To use Akismet:
As a way to better recognize between spam and ham, you can train the Akismet
filter whenever there is a false positive or false negative.
-When an entry is recognized as spam, it is rejected and added to the Spam Logs.
+When an entry is recognized as spam, it is rejected and added to the Spam Logs.
From here you can review if they are really spam. If one of them is not really
-spam, you can use the **Submit as ham** button to tell Akismet that it falsely
+spam, you can use the **Submit as ham** button to tell Akismet that it falsely
recognized an entry as spam.
![Screenshot of Spam Logs](img/spam_log.png)
diff --git a/doc/integration/auth0.md b/doc/integration/auth0.md
index a75836a915a..bccaeec3706 100644
--- a/doc/integration/auth0.md
+++ b/doc/integration/auth0.md
@@ -3,7 +3,7 @@
To enable the Auth0 OmniAuth provider, you must create an Auth0 account, and an
application.
-1. Sign in to the [Auth0 Console](https://manage.auth0.com). If you need to
+1. Sign in to the [Auth0 Console](https://auth0.com/auth/login). If you need to
create an account, you can do so at the same link.
1. Select "New App/API".
diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md
index bf587b5b296..a69db1d1a6e 100644
--- a/doc/integration/bitbucket.md
+++ b/doc/integration/bitbucket.md
@@ -54,6 +54,7 @@ you to use.
```
Account: Email, Read
+ Projects: Read
Repositories: Read
Pull Requests: Read
Issues: Read
diff --git a/doc/integration/google.md b/doc/integration/google.md
index 73e2f5826ff..b91d40d4bd4 100644
--- a/doc/integration/google.md
+++ b/doc/integration/google.md
@@ -35,7 +35,7 @@ In Google's side:
1. You should now be able to see a Client ID and Client secret. Note them down
or keep this page open as you will need them later.
-1. From the **Dashboard** select **ENABLE APIS AND SERVICES > Compute > Google+ API > Enable**
+1. From the **Dashboard** select **ENABLE APIS AND SERVICES > Social > Google+ API > Enable**
1. To enable projects to access [Google Kubernetes Engine](../user/project/clusters/index.md), you must also
enable these APIs:
- Google Kubernetes Engine API
diff --git a/doc/integration/recaptcha.md b/doc/integration/recaptcha.md
index 932cd479d56..8fdadb008ec 100644
--- a/doc/integration/recaptcha.md
+++ b/doc/integration/recaptcha.md
@@ -8,19 +8,13 @@ to confirm that a real user, not a bot, is attempting to create an account.
To use reCAPTCHA, first you must create a site and private key.
-1. Go to the URL: https://www.google.com/recaptcha/admin
-
-2. Fill out the form necessary to obtain reCAPTCHA keys.
-
-3. Login to your GitLab server, with administrator credentials.
-
-4. Go to Applications Settings on Admin Area (`admin/application_settings`)
-
-5. Fill all recaptcha fields with keys from previous steps
-
-6. Check the `Enable reCAPTCHA` checkbox
-
-7. Save the configuration.
+1. Go to the URL: <https://www.google.com/recaptcha/admin>.
+1. Fill out the form necessary to obtain reCAPTCHA keys.
+1. Login to your GitLab server, with administrator credentials.
+1. Go to Applications Settings on Admin Area (`admin/application_settings`).
+1. Fill all recaptcha fields with keys from previous steps.
+1. Check the `Enable reCAPTCHA` checkbox.
+1. Save the configuration.
## Enabling reCAPTCHA for user logins via passwords
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 2b9cce6539f..76f5495ff78 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -9,37 +9,39 @@ You can only restore a backup to **exactly the same version and type (CE/EE)**
of GitLab on which it was created. The best way to migrate your repositories
from one server to another is through backup restore.
-## Backup
+## Requirements
-GitLab provides a simple command line interface to backup your whole installation,
-and is flexible enough to fit your needs.
+In order to be able to backup and restore, you need two essential tools
+installed on your system.
-### Requirements
+### Rsync
-* rsync
+If you installed GitLab:
-If you're using GitLab with the Omnibus package, you're all set. If you
-installed GitLab from source, make sure you have rsync installed.
+- Using the Omnibus package, you're all set.
+- From source, make sure `rsync` is installed:
-If you're using Ubuntu, you could run:
+ ```sh
+ # Debian/Ubuntu
+ sudo apt-get install rsync
-```
-sudo apt-get install -y rsync
-```
+ # RHEL/CentOS
+ sudo yum install rsync
+ ```
-* tar
+### Tar
Backup and restore tasks use `tar` under the hood to create and extract
archives. Ensure you have version 1.30 or above of `tar` available in your
system. To check the version, run:
-```
+```sh
tar --version
```
-### Backup timestamp
+## Backup timestamp
->**Note:**
+NOTE: **Note:**
In GitLab 9.2 the timestamp format was changed from `EPOCH_YYYY_MM_DD` to
`EPOCH_YYYY_MM_DD_GitLab_version`, for example `1493107454_2018_04_25`
would become `1493107454_2018_04_25_10.6.4-ce`.
@@ -54,30 +56,46 @@ available.
For example, if the backup name is `1493107454_2018_04_25_10.6.4-ce_gitlab_backup.tar`,
then the timestamp is `1493107454_2018_04_25_10.6.4-ce`.
-### Creating a backup of the GitLab system
+## Creating a backup of the GitLab system
+
+GitLab provides a simple command line interface to backup your whole instance.
+It backs up your:
+
+- Database
+- Attachments
+- Git repositories data
+- CI/CD job output logs
+- CI/CD job artifacts
+- LFS objects
+- Container Registry images
+- GitLab Pages content
+
+CAUTION: **Warning:**
+GitLab does not back up any configuration files, SSL certificates, or system files.
+You are highly advised to [read about storing configuration files](#storing-configuration-files).
Use this command if you've installed GitLab with the Omnibus package:
-```
+```sh
sudo gitlab-rake gitlab:backup:create
```
Use this if you've installed GitLab from source:
-```
+```sh
sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
```
If you are running GitLab within a Docker container, you can run the backup from the host:
-```
+```sh
docker exec -t <container name> gitlab-rake gitlab:backup:create
```
If you are using the gitlab-omnibus helm chart on a Kubernetes cluster, you can
-run the backup task on the gitlab application pod using kubectl
+run the backup task on the gitlab application pod using kubectl:
-```
+```sh
kubectl exec -it <gitlab-gitlab pod> gitlab-rake gitlab:backup:create
```
@@ -110,9 +128,50 @@ Deleting tmp directories...[DONE]
Deleting old backups... [SKIPPING]
```
+## Storing configuration files
+
+A backup performed by the [raketask GitLab provides](#creating-a-backup-of-the-gitlab-system)
+does **not** store your configuration files. The primary reason for this is that your
+database contains encrypted information for two-factor authentication, the CI/CD
+'secure variables', etc. Storing encrypted information along with its key in the
+same place defeats the purpose of using encryption in the first place.
+
+CAUTION: **Warning:**
+The secrets file is essential to preserve your database encryption key.
+
+At the very **minimum**, you must backup:
+
+For Omnibus:
+
+- `/etc/gitlab/gitlab-secrets.json`
+- `/etc/gitlab/gitlab.rb`
+
+For installation from source:
+
+- `/home/git/gitlab/config/secrets.yml`
+- `/home/git/gitlab/config/gitlab.yml`
+
+For [Docker installations](https://docs.gitlab.com/omnibus/docker/), you must
+back up the volume where the configuration files are stored. If you have created
+the GitLab container according to the documentation, it should be under
+`/srv/gitlab/config`.
+
+You may also want to back up any TLS keys and certificates, and your
+[SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079).
+
+If you use Omnibus GitLab, see some additional information
+[to backup your configuration](https://docs.gitlab.com/omnibus/settings/backups.html).
+
+In the unlikely event that the secrets file is lost, see the
+[troubleshooting section](#when-the-secrets-file-is-lost).
+
+## Backup options
+
+The command line tool GitLab provides to backup your instance can take more options.
+
### Backup strategy option
-> **Note:** Introduced as an option in GitLab 8.17.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8728) in GitLab 8.17.
The default backup strategy is to essentially stream data from the respective
data locations to the backup using the Linux command `tar` and `gzip`. This works
@@ -129,8 +188,11 @@ so the problem doesn't compound, but it could be a considerable change for large
installations. This is why the `copy` strategy is not the default in 8.17.
To use the `copy` strategy instead of the default streaming strategy, specify
-`STRATEGY=copy` in the Rake task command. For example,
-`sudo gitlab-rake gitlab:backup:create STRATEGY=copy`.
+`STRATEGY=copy` in the Rake task command. For example:
+
+```sh
+sudo gitlab-rake gitlab:backup:create STRATEGY=copy
+```
### Excluding specific directories from the backup
@@ -151,11 +213,15 @@ Use a comma to specify several options at the same time:
All wikis will be backed up as part of the `repositories` group. Non-existent wikis
will be skipped during a backup.
-```
-# use this command if you've installed GitLab with the Omnibus package
+For Omnibus GitLab packages:
+
+```sh
sudo gitlab-rake gitlab:backup:create SKIP=db,uploads
+```
+
+For installations from source:
-# if you've installed GitLab from source
+```sh
sudo -u git -H bundle exec rake gitlab:backup:create SKIP=db,uploads RAILS_ENV=production
```
@@ -208,7 +274,7 @@ This example can be used for a bucket in Amsterdam (AMS3).
1. [Reconfigure GitLab] for the changes to take effect
-CAUTION: **Warning:**
+NOTE: **Note:**
If you see `400 Bad Request` by using Digital Ocean Spaces, the cause may be the
usage of backup encryption. Remove or comment the line that
contains `gitlab_rails['backup_encryption']` since Digital Ocean Spaces
@@ -370,33 +436,43 @@ backups will be copied to, and will be created if it does not exist. If the
directory that you want to copy the tarballs to is the root of your mounted
directory, just use `.` instead.
-For omnibus packages:
-```ruby
-gitlab_rails['backup_upload_connection'] = {
- :provider => 'Local',
- :local_root => '/mnt/backups'
-}
+For Omnibus GitLab packages:
-# The directory inside the mounted folder to copy backups to
-# Use '.' to store them in the root directory
-gitlab_rails['backup_upload_remote_directory'] = 'gitlab_backups'
-```
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['backup_upload_connection'] = {
+ :provider => 'Local',
+ :local_root => '/mnt/backups'
+ }
+
+ # The directory inside the mounted folder to copy backups to
+ # Use '.' to store them in the root directory
+ gitlab_rails['backup_upload_remote_directory'] = 'gitlab_backups'
+ ```
+
+1. [Reconfigure GitLab] for the changes to take effect.
+
+---
For installations from source:
-```yaml
- backup:
- # snip
- upload:
- # Fog storage connection settings, see http://fog.io/storage/ .
- connection:
- provider: Local
- local_root: '/mnt/backups'
- # The directory inside the mounted folder to copy backups to
- # Use '.' to store them in the root directory
- remote_directory: 'gitlab_backups'
-```
+1. Edit `home/git/gitlab/config/gitlab.yml`:
+
+ ```yaml
+ backup:
+ upload:
+ # Fog storage connection settings, see http://fog.io/storage/ .
+ connection:
+ provider: Local
+ local_root: '/mnt/backups'
+ # The directory inside the mounted folder to copy backups to
+ # Use '.' to store them in the root directory
+ remote_directory: 'gitlab_backups'
+ ```
+
+1. [Restart GitLab] for the changes to take effect.
### Backup archive permissions
@@ -405,45 +481,56 @@ will have owner/group git:git and 0600 permissions by default.
This is meant to avoid other system users reading GitLab's data.
If you need the backup archives to have different permissions you can use the 'archive_permissions' setting.
-```
-# In /etc/gitlab/gitlab.rb, for omnibus packages
-gitlab_rails['backup_archive_permissions'] = 0644 # Makes the backup archives world-readable
-```
+For Omnibus GitLab packages:
-```
-# In gitlab.yml, for installations from source:
- backup:
- archive_permissions: 0644 # Makes the backup archives world-readable
-```
+1. Edit `/etc/gitlab/gitlab.rb`:
-### Storing configuration files
+ ```ruby
+ gitlab_rails['backup_archive_permissions'] = 0644 # Makes the backup archives world-readable
+ ```
+
+1. [Reconfigure GitLab] for the changes to take effect.
+
+---
+
+For installations from source:
-Please be informed that a backup does not store your configuration
-files. One reason for this is that your database contains encrypted
-information for two-factor authentication. Storing encrypted
-information along with its key in the same place defeats the purpose
-of using encryption in the first place!
+1. Edit `/home/git/gitlab/config/gitlab.yml`:
-If you use an Omnibus package please see the [instructions in the readme to backup your configuration](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#backup-and-restore-omnibus-gitlab-configuration).
-If you have a cookbook installation there should be a copy of your configuration in Chef.
-If you installed from source, please consider backing up your `config/secrets.yml` file, `gitlab.yml` file, any SSL keys and certificates, and your [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079).
+ ```yaml
+ backup:
+ archive_permissions: 0644 # Makes the backup archives world-readable
+ ```
-At the very **minimum** you should backup `/etc/gitlab/gitlab.rb` and
-`/etc/gitlab/gitlab-secrets.json` (Omnibus), or
-`/home/git/gitlab/config/secrets.yml` (source) to preserve your database
-encryption key.
+1. [Restart GitLab] for the changes to take effect.
### Configuring cron to make daily backups
->**Note:**
+NOTE: **Note:**
The following cron jobs do not [backup your GitLab configuration files](#storing-configuration-files)
or [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079).
-**For Omnibus installations**
+For Omnibus GitLab packages:
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ ## Limit backup lifetime to 7 days - 604800 seconds
+ gitlab_rails['backup_keep_time'] = 604800
+ ```
+
+1. [Reconfigure GitLab] for the changes to take effect.
+
+Note that the `backup_keep_time` configuration option only manages local
+files. GitLab does not automatically prune old files stored in a third-party
+object storage (e.g., AWS S3) because the user may not have permission to list
+and delete files. We recommend that you configure the appropriate retention
+policy for your object storage. For example, you can configure [the S3 backup
+policy as described here](http://stackoverflow.com/questions/37553070/gitlab-omnibus-delete-backup-from-amazon-s3).
To schedule a cron job that backs up your repositories and GitLab metadata, use the root user:
-```
+```sh
sudo su -
crontab -e
```
@@ -455,26 +542,24 @@ There, add the following line to schedule the backup for everyday at 2 AM:
```
You may also want to set a limited lifetime for backups to prevent regular
-backups using all your disk space. To do this add the following lines to
-`/etc/gitlab/gitlab.rb` and reconfigure:
+backups using all your disk space.
-```
-# limit backup lifetime to 7 days - 604800 seconds
-gitlab_rails['backup_keep_time'] = 604800
-```
+---
-Note that the `backup_keep_time` configuration option only manages local
-files. GitLab does not automatically prune old files stored in a third-party
-object storage (e.g., AWS S3) because the user may not have permission to list
-and delete files. We recommend that you configure the appropriate retention
-policy for your object storage. For example, you can configure [the S3 backup
-policy as described here](http://stackoverflow.com/questions/37553070/gitlab-omnibus-delete-backup-from-amazon-s3).
+For installations from source:
+
+1. Edit `home/git/gitlab/config/gitlab.yml`:
-**For installation from source**
+ ```yaml
+ backup:
+ ## Limit backup lifetime to 7 days - 604800 seconds
+ keep_time: 604800
+ ```
-```
-cd /home/git/gitlab
-sudo -u git -H editor config/gitlab.yml # Enable keep_time in the backup section to automatically delete old backups
+1. [Restart GitLab] for the changes to take effect.
+
+
+```sh
sudo -u git crontab -e # Edit the crontab for the git user
```
@@ -711,5 +796,53 @@ Those objects have no influence on the database backup/restore but they give thi
For more information see similar questions on postgresql issue tracker[here](http://www.postgresql.org/message-id/201110220712.30886.adrian.klaver@gmail.com) and [here](http://www.postgresql.org/message-id/2039.1177339749@sss.pgh.pa.us) as well as [stack overflow](http://stackoverflow.com/questions/4368789/error-must-be-owner-of-language-plpgsql).
+### When the secrets file is lost
+
+If you have failed to [back up the secrets file](#storing-configuration-files),
+then users with 2FA enabled will not be able to log into GitLab. In that case,
+you need to [disable 2FA for everyone](../security/two_factor_authentication.md#disabling-2fa-for-everyone).
+
+In the case of CI/CD, if your project has secure variables set, you might experience
+some weird behavior, like stuck jobs or 500 errors. In that case, you can try
+deleting the `ci_variables` table from the database.
+
+CAUTION: **Warning:**
+Use the following commands at your own risk, and make sure you've taken a
+backup beforehand.
+
+1. Enter the Rails console:
+
+ For Omnibus GitLab packages:
+
+ ```sh
+ sudo gitlab-rails dbconsole
+ ```
+
+ For installations from source:
+
+ ```sh
+ sudo -u git -H bundle exec rails dbconsole RAILS_ENV=production
+ ```
+
+1. Check the `ci_variables` table:
+
+ ```sql
+ SELECT * FROM public."ci_variables";
+ ```
+
+ Those are the variables that you need to delete.
+
+1. Drop the table:
+
+ ```sql
+ DELETE FROM ci_variables;
+ ```
+
+1. You may need to reconfigure or restart GitLab for the changes to take
+ effect.
+
+You should now be able to visit your project, and the jobs will start
+running again.
+
[reconfigure GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
diff --git a/doc/raketasks/import.md b/doc/raketasks/import.md
index 97e9b36d1a6..bb316df5b9a 100644
--- a/doc/raketasks/import.md
+++ b/doc/raketasks/import.md
@@ -6,6 +6,7 @@
- The groups will be created as needed, including subgroups
- The owner of the group will be the first admin
- Existing projects will be skipped
+- Projects in hashed storage may be skipped (see [Importing bare repositories from hashed storage](#importing-bare-repositories-from-hashed-storage))
- The existing Git repos will be moved from disk (removed from the original path)
## How to use
@@ -26,7 +27,6 @@ sudo -u git mkdir /var/opt/gitlab/git-data/repository-import-<date>/new_group
If we copy the repos to `/var/opt/gitlab/git-data/repository-import-<date>`, and repo A needs to be under the groups G1 and G2, it will
have to be created under those folders: `/var/opt/gitlab/git-data/repository-import-<date>/G1/G2/A.git`.
-
```
sudo cp -r /old/git/foo.git /var/opt/gitlab/git-data/repository-import-<date>/new_group/
@@ -70,3 +70,73 @@ Processing /var/opt/gitlab/git-data/repository-import-1/group/xyz.git
* Skipping repo /var/opt/gitlab/git-data/repository-import-1/@shared/a/b/abcd.git
[...]
```
+
+## Importing bare repositories from hashed storage
+
+### Background
+
+Projects in legacy storage have a directory structure that mirrors their full
+project path in GitLab, including their namespace structure. This information is
+leveraged by the bare repository importer to import projects into their proper
+locations. Each project and its parent namespaces are meaningfully named.
+
+However, the directory structure of projects in hashed storage do not contain
+this information. This is beneficial for a variety of reasons, especially
+improved performance and data integrity. See
+[Repository Storage Types](../administration/repository_storage_types.md) for
+more details.
+
+### Which repositories are importable?
+
+#### GitLab 10.3 or earlier
+
+Importing bare repositories from hashed storage is unsupported.
+
+#### GitLab 10.4 and later
+
+To support importing bare repositories from hashed storage, GitLab 10.4 and
+later stores the full project path with each repository, in a special section of
+the git repository's config file. This section is formatted as follows:
+
+```
+[gitlab]
+ fullpath = gitlab-org/gitlab-ce
+```
+
+However, existing repositories were not migrated to include this path.
+
+Bare repositories are importable if the following events occurred to the
+repository in GitLab 10.4 and later:
+
+- Created
+- Migrated to hashed storage
+- Renamed
+- Transferred to another namespace
+- Ancestor renamed
+- Ancestor transferred to another namespace
+
+Bare repositories are **not** importable by GitLab 10.4 and later when all the following are true about the repository:
+
+- It was created in GitLab 10.3 or earlier.
+- It was not renamed, transferred, or migrated to hashed storage in GitLab 10.4 and later.
+- Its ancestor namespaces were not renamed or transferred in GitLab 10.4 and later.
+
+There is an [open issue to add a migration to make all bare repositories
+importable](https://gitlab.com/gitlab-org/gitlab-ce/issues/41776).
+
+Until then, you may wish to manually migrate repositories yourself. You can use
+[Rails console](https://docs.gitlab.com/omnibus/maintenance/#starting-a-rails-console-session)
+to do so. In a Rails console session, run the following to migrate a project:
+
+```
+project = Project.find_by_full_path('gitlab-org/gitlab-ce')
+project.write_repository_config
+```
+
+In a Rails console session, run the following to migrate all of a namespace's
+projects (this may take a while if there are 1000s of projects in a namespace):
+
+```
+namespace = Namespace.find_by_full_path('gitlab-org')
+namespace.send(:write_projects_repository_config)
+```
diff --git a/doc/security/rack_attack.md b/doc/security/rack_attack.md
index 3efb19c1526..07e7b3da13b 100644
--- a/doc/security/rack_attack.md
+++ b/doc/security/rack_attack.md
@@ -10,7 +10,7 @@ Rack Attack offers IP whitelisting, blacklisting, Fail2ban style filtering and
tracking.
**Note:** Starting with 11.2, Rack Attack is disabled by default. To continue
-using this feature, please enable it in your `gitlab.rb` by setting
+using this feature, please enable it in your `gitlab.rb` by setting
`gitlab_rails['rack_attack_git_basic_auth'] = true`.
By default, user sign-in, user sign-up (if enabled), and user password reset is
@@ -41,7 +41,7 @@ For more information on how to use these options check out
}
```
-3. Reconfigure GitLab:
+1. Reconfigure GitLab:
```
sudo gitlab-ctl reconfigure
@@ -98,26 +98,26 @@ In case you want to remove a blocked IP, follow these steps:
grep "Rack_Attack" /var/log/gitlab/gitlab-rails/production.log
```
-2. Since the blacklist is stored in Redis, you need to open up `redis-cli`:
+1. Since the blacklist is stored in Redis, you need to open up `redis-cli`:
```sh
/opt/gitlab/embedded/bin/redis-cli -s /var/opt/gitlab/redis/redis.socket
```
-3. You can remove the block using the following syntax, replacing `<ip>` with
+1. You can remove the block using the following syntax, replacing `<ip>` with
the actual IP that is blacklisted:
```
del cache:gitlab:rack::attack:allow2ban:ban:<ip>
```
-4. Confirm that the key with the IP no longer shows up:
+1. Confirm that the key with the IP no longer shows up:
```
keys *rack::attack*
```
-5. Optionally, add the IP to the whitelist to prevent it from being blacklisted
+1. Optionally, add the IP to the whitelist to prevent it from being blacklisted
again (see [settings](#settings)).
## Troubleshooting
@@ -129,11 +129,11 @@ the load balancer. In that case, you will need to:
1. [Configure `nginx[real_ip_trusted_addresses]`](https://docs.gitlab.com/omnibus/settings/nginx.html#configuring-gitlab-trusted_proxies-and-the-nginx-real_ip-module).
This will keep users' IPs from being listed as the load balancer IPs.
-2. Whitelist the load balancer's IP address(es) in the Rack Attack [settings](#settings).
-3. Reconfigure GitLab:
+1. Whitelist the load balancer's IP address(es) in the Rack Attack [settings](#settings).
+1. Reconfigure GitLab:
```
sudo gitlab-ctl reconfigure
```
-4. [Remove the block via Redis.](#remove-blocked-ips-from-rack-attack-via-redis)
+1. [Remove the block via Redis.](#remove-blocked-ips-from-rack-attack-via-redis)
diff --git a/doc/security/two_factor_authentication.md b/doc/security/two_factor_authentication.md
index cd290a80314..b770f2544d2 100644
--- a/doc/security/two_factor_authentication.md
+++ b/doc/security/two_factor_authentication.md
@@ -13,8 +13,8 @@ You can read more about it here:
Users on GitLab, can enable it without any admin's intervention. If you want to
enforce everyone to set up 2FA, you can choose from two different ways:
- 1. Enforce on next login
- 2. Suggest on next login, but allow a grace period before enforcing.
+- Enforce on next login.
+- Suggest on next login, but allow a grace period before enforcing.
In the Admin area under **Settings** (`/admin/application_settings`), look for
the "Sign-in Restrictions" area, where you can configure both.
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index 5db042326f3..c5b7813b285 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -8,163 +8,224 @@ you need a secure communication channel for sharing information.
The SSH protocol provides this security and allows you to authenticate to the
GitLab remote server without supplying your username or password each time.
-For a more detailed explanation of how the SSH protocol works, we advise you to
-read [this nice tutorial by DigitalOcean](https://www.digitalocean.com/community/tutorials/understanding-the-ssh-encryption-and-connection-process).
+For a more detailed explanation of how the SSH protocol works, read
+[this nice tutorial by DigitalOcean](https://www.digitalocean.com/community/tutorials/understanding-the-ssh-encryption-and-connection-process).
-## Locating an existing SSH key pair
+## Requirements
-Before generating a new SSH key pair check if your system already has one
-at the default location by opening a shell, or Command Prompt on Windows,
-and running the following command:
+The only requirement is to have the OpenSSH client installed on your system. This
+comes pre-installed on GNU/Linux and macOS, but not on Windows.
-**Windows Command Prompt:**
+Depending on your Windows version, there are different methods to work with
+SSH keys.
-```bash
-type %userprofile%\.ssh\id_rsa.pub
-```
+### Installing the SSH client for Windows 10
-**Git Bash on Windows / GNU/Linux / macOS / PowerShell:**
+Starting with Windows 10, you can
+[install the Windows Subsystem for Linux (WSL)](https://docs.microsoft.com/en-us/windows/wsl/install-win10)
+where you can run Linux distributions directly on Windows, without the overhead
+of a virtual machine. Once installed and set up, you'll have the Git and SSH
+clients at your disposal.
-```bash
-cat ~/.ssh/id_rsa.pub
-```
+### Installing the SSH client for Windows 8.1 and Windows 7
+
+The easiest way to install Git and the SSH client on Windows 8.1 and Windows 7
+is [Git for Windows](https://gitforwindows.com). It provides a BASH
+emulation (Git Bash) used for running Git from the command line and the
+`ssh-keygen` command that is useful to create SSH keys as you'll learn below.
+
+NOTE: **Alternative tools:**
+Although not explored in this page, you can use some alternative tools.
+[Cygwin](https://www.cygwin.com) is a large collection of GNU and open source
+tools which provide functionality similar to a Unix distribution.
+[PuttyGen](https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html)
+provides a graphical user interface to [create SSH keys](https://tartarus.org/~simon/putty-snapshots/htmldoc/Chapter8.html#pubkey-puttygen).
+
+## Types of SSH keys and which to choose
+
+GitLab supports RSA, DSA, ECDSA, and ED25519 keys. Their difference lies on
+the signing algorithm, and some of them have advantages over the others. For
+more information, you can read this
+[nice article on ArchWiki](https://wiki.archlinux.org/index.php/SSH_keys#Choosing_the_authentication_key_type).
+We'll focus on ED25519 and RSA and here.
+
+NOTE: **Note:**
+As an admin, you can restrict
+[which keys should be permitted and their minimum length](../security/ssh_keys_restrictions.md).
+By default, all keys are permitted, which is also the case for
+[GitLab.com](../user/gitlab_com/index.md#ssh-host-keys-fingerprints).
-If you see a string starting with `ssh-rsa` you already have an SSH key pair
-and you can skip the generate portion of the next section and skip to the copy
-to clipboard step.
-If you don't see the string or would like to generate a SSH key pair with a
-custom name continue onto the next step.
+## ED25519 SSH keys
-Note that Public SSH key may also be named as follows:
+Following [best practices](https://linux-audit.com/using-ed25519-openssh-keys-instead-of-dsa-rsa-ecdsa/),
+you should always favor [ED25519](https://ed25519.cr.yp.to/) SSH keys, since they
+are more secure and have better performance over the other types.
-- `id_dsa.pub`
-- `id_ecdsa.pub`
-- `id_ed25519.pub`
+They were introduced in OpenSSH 6.5, so any modern OS should include the
+option to create them. If for any reason your OS or the GitLab instance you
+interact with doesn't support this, you can fallback to RSA.
+
+## RSA SSH keys
+
+RSA keys are the most common ones and therefore the most compatible with
+servers that may have an old OpenSSH version. Use them if the GitLab server
+doesn't work with ED25519 keys.
+
+The minimum key size is 1024 bits, defaulting to 2048. If you wish to generate a
+stronger RSA key pair, specify the `-b` flag with a higher bit value than the
+default.
+
+The old, default password encoding for SSH private keys keys is
+[insecure](https://latacora.singles/2018/08/03/the-default-openssh.html);
+it's only a single round of an MD5 hash. Since OpenSSH version 6.5, you should
+use the `-o` option to `ssh-keygen` to encode your private key in a new, more
+secure format.
+
+If you already have an RSA SSH key pair to use with GitLab, consider upgrading it
+to use the more secure password encryption format by using the following command
+on the private key:
+
+```bash
+ssh-keygen -o -f ~/.ssh/id_rsa
+```
## Generating a new SSH key pair
-1. To generate a new SSH key pair, use the following command:
+Before creating an SSH key pair, make sure to read about the
+[different types of keys](#types-of-ssh-keys-and-which-to-choose) to understand
+their differences.
+
+To create a new SSH key pair:
- **Git Bash on Windows / GNU/Linux / macOS:**
+1. Open a terminal on Linux or macOS, or Git Bash / WSL on Windows.
+1. Generate a new ED25519 SSH key pair:
```bash
- ssh-keygen -o -t rsa -C "your.email@example.com" -b 4096
+ ssh-keygen -t ed25519 -C "email@example.com"
```
- (Note: the `-o` option was introduced in 2014; if this command does not work for you, simply remove the `-o` option and try again)
+ Or, if you want to use RSA:
- **Windows:**
+ ```bash
+ ssh-keygen -o -t rsa -b 4096 -C "email@example.com"
+ ```
- Alternatively on Windows you can download
- [PuttyGen](http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html)
- and follow [this documentation article][winputty] to generate a SSH key pair.
+ The `-C` flag adds a comment in the key in case you have multiple of them
+ and want to tell which is which. It is optional.
1. Next, you will be prompted to input a file path to save your SSH key pair to.
+ If you don't already have an SSH key pair, use the suggested path by pressing
+ <kbd>Enter</kbd>. Using the suggested path will normally allow your SSH client
+ to automatically use the SSH key pair with no additional configuration.
- If you don't already have an SSH key pair use the suggested path by pressing
- enter. Using the suggested path will normally allow your SSH client
- to automatically use the SSH key pair with no additional configuration.
+ If you already have an SSH key pair with the suggested file path, you will need
+ to input a new file path and [declare what host](#working-with-non-default-ssh-key-pair-paths)
+ this SSH key pair will be used for in your `~/.ssh/config` file.
- If you already have a SSH key pair with the suggested file path, you will need
- to input a new file path and declare what host this SSH key pair will be used
- for in your `.ssh/config` file, see [**Working with non-default SSH key pair paths**](#working-with-non-default-ssh-key-pair-paths)
- for more information.
+1. Once the path is decided, you will be prompted to input a password to
+ secure your new SSH key pair. It's a best practice to use a password,
+ but it's not required and you can skip creating it by pressing
+ <kbd>Enter</kbd> twice.
-1. Once you have input a file path you will be prompted to input a password to
- secure your SSH key pair. It is a best practice to use a password for an SSH
- key pair, but it is not required and you can skip creating a password by
- pressing enter.
+ If, in any case, you want to add or change the password of your SSH key pair,
+ you can use the `-p`flag:
- NOTE: **Note:**
- If you want to change the password of your SSH key pair, you can use
- `ssh-keygen -p -o -f <keyname>`.
- The `-o` option was added in 2014, so if this command does not work for you,
- simply remove the `-o` option and try again.
+ ```
+ ssh-keygen -p -o -f <keyname>
+ ```
-## Adding a SSH key to your GitLab account
+Now, it's time to add the newly created public key to your GitLab account.
-1. The next step is to copy the public SSH key as we will need it afterwards.
+## Adding an SSH key to your GitLab account
- To copy your public SSH key to the clipboard, use the appropriate code below:
+1. Copy your **public** SSH key to the clipboard by using one of the commands below
+ depending on your Operating System:
**macOS:**
```bash
- pbcopy < ~/.ssh/id_rsa.pub
+ pbcopy < ~/.ssh/id_ed25519.pub
```
- **GNU/Linux (requires the xclip package):**
+ **WSL / GNU/Linux (requires the xclip package):**
```bash
- xclip -sel clip < ~/.ssh/id_rsa.pub
+ xclip -sel clip < ~/.ssh/id_ed25519.pub
```
- **Windows Command Line:**
+ **Git Bash on Windows:**
```bash
- type %userprofile%\.ssh\id_rsa.pub | clip
+ cat ~/.ssh/id_ed25519.pub | clip
```
- **Git Bash on Windows / Windows PowerShell:**
+ You can also open the key in a graphical editor and copy it from there,
+ but be careful not to accidentally change anything.
- ```bash
- cat ~/.ssh/id_rsa.pub | clip
- ```
-
-1. The final step is to add your public SSH key to GitLab.
+ NOTE: **Note:**
+ If you opted to create an RSA key, the name might differ.
- Navigate to the 'SSH Keys' tab in your 'Profile Settings'.
- Paste your key in the 'Key' section and give it a relevant 'Title'.
- Use an identifiable title like 'Work Laptop - Windows 7' or
- 'Home MacBook Pro 15'.
+1. Add your public SSH key to your GitLab account by clicking your avatar
+ in the upper right corner and selecting **Settings**. From there on,
+ navigate to **SSH Keys** and paste your public key in the "Key" section.
+ If you created the key with a comment, this will appear under "Title".
+ If not, give your key an identifiable title like _Work Laptop_ or
+ _Home Workstation_, and click **Add key**.
+ NOTE: **Note:**
If you manually copied your public SSH key make sure you copied the entire
- key starting with `ssh-rsa` and ending with your email.
+ key starting with `ssh-ed25519` (or `ssh-rsa`) and ending with your email.
+
+## Testing that everything is set up correctly
+
+To test whether your SSH key was added correctly, run the following command in
+your terminal (replacing `gitlab.com` with your GitLab's instance domain):
-1. Optionally you can test your setup by running `ssh -T git@example.com`
- (replacing `example.com` with your GitLab domain) and verifying that you
- receive a `Welcome to GitLab` message.
+```bash
+ssh -T git@gitlab.com
+```
+
+You should receive a _Welcome to GitLab, `@username`!_ message.
+
+If the welcome message doesn't appear, run SSH's verbose mode by replacing `-T`
+with `-vvvT` to understand where the error is.
## Working with non-default SSH key pair paths
If you used a non-default file path for your GitLab SSH key pair,
you must configure your SSH client to find your GitLab private SSH key
-for connections to your GitLab server (perhaps `gitlab.com`).
+for connections to GitLab.
-For your current terminal session you can do so using the following commands
+Open a terminal and use the following commands
(replacing `other_id_rsa` with your private SSH key):
-**Git Bash on Windows / GNU/Linux / macOS:**
-
```bash
eval $(ssh-agent -s)
ssh-add ~/.ssh/other_id_rsa
```
-To retain these settings you'll need to save them to a configuration file.
-For OpenSSH clients this is configured in the `~/.ssh/config` file for some
-operating systems.
+To retain these settings, you'll need to save them to a configuration file.
+For OpenSSH clients this is configured in the `~/.ssh/config` file. In this
+file you can set up configurations for multiple hosts, like GitLab.com, your
+own GitLab instance, GitHub, Bitbucket, etc.
+
Below are two example host configurations using their own SSH key:
-```
-# GitLab.com server
+```conf
+# GitLab.com
Host gitlab.com
-RSAAuthentication yes
-IdentityFile ~/.ssh/config/private-key-filename-01
+ Preferredauthentications publickey
+ IdentityFile ~/.ssh/gitlab_com_rsa
-# Private GitLab server
+# Private GitLab instance
Host gitlab.company.com
-RSAAuthentication yes
-IdentityFile ~/.ssh/config/private-key-filename
+ Preferredauthentications publickey
+ IdentityFile ~/.ssh/example_com_rsa
```
-Due to the wide variety of SSH clients and their very large number of
-configuration options, further explanation of these topics is beyond the scope
-of this document.
-
-Public SSH keys need to be unique, as they will bind to your account.
-Your SSH key is the only identifier you'll have when pushing code via SSH.
-That's why it needs to uniquely map to a single user.
+Public SSH keys need to be unique to GitLab, as they will bind to your account.
+Your SSH key is the only identifier you'll have when pushing code via SSH,
+that's why it needs to uniquely map to a single user.
## Deploy keys
@@ -240,8 +301,6 @@ not implicitly give any access just by setting them up.
How to add your SSH key to Eclipse: https://wiki.eclipse.org/EGit/User_Guide#Eclipse_SSH_Configuration
-[winputty]: https://the.earth.li/~sgtatham/putty/0.67/htmldoc/Chapter8.html#pubkey-puttygen
-
## SSH on the GitLab server
GitLab integrates with the system-installed SSH daemon, designating a user
diff --git a/doc/topics/authentication/index.md b/doc/topics/authentication/index.md
index 73301394e9f..394f3ea60b7 100644
--- a/doc/topics/authentication/index.md
+++ b/doc/topics/authentication/index.md
@@ -36,7 +36,7 @@ This page gathers all the resources for the topic **Authentication** within GitL
## API
- [OAuth 2 Tokens](../../api/README.md#oauth-2-tokens)
-- [Private Tokens](../../api/README.md#private-tokens)
+- [Personal access tokens](../../api/README.md#personal-access-tokens)
- [Impersonation tokens](../../api/README.md#impersonation-tokens)
- [GitLab as an OAuth2 provider](../../api/oauth2.md#gitlab-as-an-oauth2-provider)
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 96e788666a1..3647f600b21 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -586,7 +586,7 @@ repo or by specifying a project variable:
file in it, Auto DevOps will detect the chart and use it instead of the [default
one](https://gitlab.com/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#secret-variables)
+- **Project variable** - Create a [project variable](../../ci/variables/README.md#variables)
`AUTO_DEVOPS_CHART` with the URL of a custom chart to use.
### Customizing `.gitlab-ci.yml`
@@ -660,7 +660,7 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac
TIP: **Tip:**
Set up the replica variables using a
-[project variable](../../ci/variables/README.md#secret-variables)
+[project variable](../../ci/variables/README.md#variables)
and scale your application by just redeploying it!
CAUTION: **Caution:**
@@ -738,7 +738,7 @@ staging environment and deploy to production manually. For this scenario, the
`STAGING_ENABLED` environment variable was introduced.
If `STAGING_ENABLED` is defined in your project (e.g., set `STAGING_ENABLED` to
-`1` as a secret variable), then the application will be automatically deployed
+`1` as a CI/CD variable), then the application will be automatically deployed
to a `staging` environment, and a `production_manual` job will be created for
you when you're ready to manually deploy to production.
@@ -751,7 +751,7 @@ A [canary environment](https://docs.gitlab.com/ee/user/project/canary_deployment
before any changes are deployed to production.
If `CANARY_ENABLED` is defined in your project (e.g., set `CANARY_ENABLED` to
-`1` as a secret variable) then two manual jobs will be created:
+`1` as a CI/CD variable) then two manual jobs will be created:
- `canary` which will deploy the application to the canary environment
- `production_manual` which is to be used by you when you're ready to manually
diff --git a/doc/topics/git/troubleshooting_git.md b/doc/topics/git/troubleshooting_git.md
index 8555c5e91ea..d1729d70158 100644
--- a/doc/topics/git/troubleshooting_git.md
+++ b/doc/topics/git/troubleshooting_git.md
@@ -78,5 +78,20 @@ 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]).
+## Timeout during git push/pull
+
+If pulling/pushing from/to your repository ends up taking more than 50 seconds,
+a timeout will be issued with a log of the number of operations performed
+and their respective timings, like the example below:
+
+```
+remote: Running checks for branch: master
+remote: Scanning for LFS objects... (153ms)
+remote: Calculating new repository size... (cancelled after 729ms)
+```
+
+This could be used to further investigate what operation is performing poorly
+and provide GitLab with more information on how to improve the service.
+
[SSH troubleshooting]: ../../ssh/README.md#troubleshooting "SSH Troubleshooting"
[Broken-Pipe]: https://stackoverflow.com/questions/19120120/broken-pipe-when-pushing-to-git-repository/36971469#36971469 "StackOverflow: 'Broken pipe when pushing to Git repository'"
diff --git a/doc/university/README.md b/doc/university/README.md
index f19b1ffd3d9..3e7d02770e4 100644
--- a/doc/university/README.md
+++ b/doc/university/README.md
@@ -104,7 +104,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
1. [Due Dates and Milestones for GitLab Issues](https://about.gitlab.com/2016/08/05/feature-highlight-set-dates-for-issues/)
1. [How to Use GitLab Labels](https://about.gitlab.com/2016/08/17/using-gitlab-labels/)
1. [Applying GitLab Labels Automatically](https://about.gitlab.com/2016/08/19/applying-gitlab-labels-automatically/)
-1. [GitLab Issue Board - Product Page](https://about.gitlab.com/solutions/issueboard/)
+1. [GitLab Issue Board - Product Page](https://about.gitlab.com/product/issueboard/)
1. [An Overview of GitLab Issue Board](https://about.gitlab.com/2016/08/22/announcing-the-gitlab-issue-board/)
1. [Designing GitLab Issue Board](https://about.gitlab.com/2016/08/31/designing-issue-boards/)
1. [From Idea to Production with GitLab - Video](https://www.youtube.com/watch?v=25pHyknRgEo&index=14&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
@@ -125,7 +125,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
1. [Setting up GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
1. [IBM: Continuous Delivery vs Continuous Deployment - Video](https://www.youtube.com/watch?v=igwFj8PPSnw)
1. [Amazon: Transition to Continuous Delivery - Video](https://www.youtube.com/watch?v=esEFaY0FDKc)
-2. [TechBeacon: Doing continuous delivery? Focus first on reducing release cycle times](https://techbeacon.com/doing-continuous-delivery-focus-first-reducing-release-cycle-times)
+1. [TechBeacon: Doing continuous delivery? Focus first on reducing release cycle times](https://techbeacon.com/doing-continuous-delivery-focus-first-reducing-release-cycle-times)
1. See **[Integrations](#39-integrations)** for integrations with other CI services.
#### 2.4. Workflow
@@ -140,7 +140,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
1. [GitLab Compared to Other Tools](https://about.gitlab.com/comparison/)
1. [Comparing GitLab Terminology](https://about.gitlab.com/2016/01/27/comparing-terms-gitlab-github-bitbucket/)
-1. [GitLab Compared to Atlassian (Recording 2016-03-03) ](https://youtu.be/Nbzp1t45ERo)
+1. [GitLab Compared to Atlassian (Recording 2016-03-03)](https://youtu.be/Nbzp1t45ERo)
1. [GitLab Position FAQ](https://about.gitlab.com/handbook/positioning-faq)
1. [Customer review of GitLab with points on why they prefer GitLab](https://www.enovate.co.uk/web-design-blog/2015/11/25/gitlab-review/)
@@ -189,7 +189,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
#### 3.8 Cycle Analytics
1. [GitLab Cycle Analytics Overview](https://about.gitlab.com/2016/09/21/cycle-analytics-feature-highlight/)
-1. [GitLab Cycle Analytics - Product Page](https://about.gitlab.com/solutions/cycle-analytics/)
+1. [GitLab Cycle Analytics - Product Page](https://about.gitlab.com/product/cycle-analytics/)
#### 3.9. Integrations
@@ -213,7 +213,8 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
### 5. Resources for GitLab Team Members
-*Some content can only be accessed by GitLab team members*
+NOTE: **Note:**
+Some content can only be accessed by GitLab team members
1. [Support Path](support/README.md)
1. [Sales Path (redirect to sales handbook)](https://about.gitlab.com/handbook/sales-onboarding/)
diff --git a/doc/university/training/end-user/README.md b/doc/university/training/end-user/README.md
index e5eb5d97e3b..701533358c8 100644
--- a/doc/university/training/end-user/README.md
+++ b/doc/university/training/end-user/README.md
@@ -78,7 +78,7 @@ Workshop Time!
```bash
git config --global user.name "Your Name"
git config --global user.email you@example.com
-```
+```
- If you don't use the global flag you can set up a different author for
each project
@@ -107,14 +107,14 @@ cd ~/development
-or-
mkdir ~/workspace
-cd ~/workspace
+cd ~/workspace
```
---
## Git Basics
----
+---
### Git Workflow
@@ -136,7 +136,7 @@ cd ~/workspace
issue tracking, Merge Requests, and other features.
- The hosted version of GitLab is gitlab.com
----
+---
### New Project
@@ -150,12 +150,12 @@ cd ~/workspace
### Git and GitLab basics
1. Edit `edit_this_file.rb` in `training-examples`
-2. See it listed as a changed file (working area)
-3. View the differences
-4. Stage the file
-5. Commit
-6. Push the commit to the remote
-7. View the git log
+1. See it listed as a changed file (working area)
+1. View the differences
+1. Stage the file
+1. Commit
+1. Push the commit to the remote
+1. View the git log
---
@@ -169,14 +169,14 @@ git push origin master
git log
```
----
+---
### Feature Branching
1. Create a new feature branch called `squash_some_bugs`
-2. Edit `bugs.rb` and remove all the bugs.
-3. Commit
-4. Push
+1. Edit `bugs.rb` and remove all the bugs.
+1. Commit
+1. Push
---
@@ -250,16 +250,17 @@ git push origin squash_some_bugs
---
### Example Plan
+
1. Checkout a new branch and edit conflicts.rb. Add 'Line4' and 'Line5'.
-2. Commit and push
-3. Checkout master and edit conflicts.rb. Add 'Line6' and 'Line7' below 'Line3'.
-4. Commit and push to master
-5. Create a merge request and watch it fail
-6. Rebase our new branch with master
-7. Fix conflicts on the conflicts.rb file.
-8. Stage the file and continue rebasing
-9. Force push the changes
-10. Finally continue with the Merge Request
+1. Commit and push
+1. Checkout master and edit conflicts.rb. Add 'Line6' and 'Line7' below 'Line3'.
+1. Commit and push to master
+1. Create a merge request and watch it fail
+1. Rebase our new branch with master
+1. Fix conflicts on the conflicts.rb file.
+1. Stage the file and continue rebasing
+1. Force push the changes
+1. Finally continue with the Merge Request
---
@@ -362,15 +363,15 @@ Don't reset after pushing
### Reset Workflow
1. Edit file again 'edit_this_file.rb'
-2. Check status
-3. Add and commit with wrong message
-4. Check log
-5. Amend commit
-6. Check log
-7. Soft reset
-8. Check log
-9. Pull for updates
-10. Push changes
+1. Check status
+1. Add and commit with wrong message
+1. Check log
+1. Amend commit
+1. Check log
+1. Soft reset
+1. Check log
+1. Pull for updates
+1. Push changes
----
@@ -389,9 +390,9 @@ Don't reset after pushing
### Note
-git revert vs git reset
-Reset removes the commit while revert removes the changes but leaves the commit
-Revert is safer considering we can revert a revert
+git revert vs git reset
+Reset removes the commit while revert removes the changes but leaves the commit
+Revert is safer considering we can revert a revert
# Changed file
diff --git a/doc/university/training/topics/additional_resources.md b/doc/university/training/topics/additional_resources.md
index d01634df744..4871372d105 100644
--- a/doc/university/training/topics/additional_resources.md
+++ b/doc/university/training/topics/additional_resources.md
@@ -4,9 +4,9 @@ comments: false
# Additional Resources
-1. GitLab Documentation [http://docs.gitlab.com](http://docs.gitlab.com/)
-2. GUI Clients [http://git-scm.com/downloads/guis](http://git-scm.com/downloads/guis)
-3. Pro git book [http://git-scm.com/book](http://git-scm.com/book)
-4. Platzi Course [https://courses.platzi.com/courses/git-gitlab/](https://courses.platzi.com/courses/git-gitlab/)
-5. Code School tutorial [http://try.github.io/](http://try.github.io/)
-6. Contact Us at `subscribers@gitlab.com`
+1. GitLab Documentation: <http://docs.gitlab.com>.
+1. GUI Clients: <http://git-scm.com/downloads/guis>.
+1. Pro Git book: <http://git-scm.com/book>.
+1. Platzi Course: <https://courses.platzi.com/courses/git-gitlab/>.
+1. Code School tutorial: <http://try.github.io/>.
+1. Contact us at `subscribers@gitlab.com`.
diff --git a/doc/university/training/topics/bisect.md b/doc/university/training/topics/bisect.md
index 01e93e4dcb0..4848d0412c1 100644
--- a/doc/university/training/topics/bisect.md
+++ b/doc/university/training/topics/bisect.md
@@ -2,7 +2,7 @@
comments: false
---
-# Bisect
+# Bisect
----------
@@ -17,11 +17,11 @@ comments: false
## Bisect
1. Start the bisect process
-2. Enter the bad revision (usually latest commit)
-3. Enter a known good revision (commit/branch)
-4. Run code to see if bug still exists
-5. Tell bisect the result
-6. Repeat the previous 2 items until you find the offending commit
+1. Enter the bad revision (usually latest commit)
+1. Enter a known good revision (commit/branch)
+1. Run code to see if bug still exists
+1. Tell bisect the result
+1. Repeat the previous 2 items until you find the offending commit
----------
diff --git a/doc/university/training/topics/getting_started.md b/doc/university/training/topics/getting_started.md
index 1441e4b89b2..66cb08feacb 100644
--- a/doc/university/training/topics/getting_started.md
+++ b/doc/university/training/topics/getting_started.md
@@ -35,11 +35,10 @@ comments: false
## Instantiate workflow with clone
-1. Create a project in your user namespace
- - Choose to import from 'Any Repo by URL' and use
- https://gitlab.com/gitlab-org/training-examples.git
-2. Create a '`Workspace`' directory in your home directory.
-3. Clone the '`training-examples`' project
+1. Create a project in your user namespace.
+ - Choose to import from 'Any Repo by URL' and use <https://gitlab.com/gitlab-org/training-examples.git>.
+1. Create a '`Workspace`' directory in your home directory.
+1. Clone the '`training-examples`' project.
----------
diff --git a/doc/university/training/topics/git_log.md b/doc/university/training/topics/git_log.md
index 3e39ea5cc9a..6ba6f9eb69d 100644
--- a/doc/university/training/topics/git_log.md
+++ b/doc/university/training/topics/git_log.md
@@ -46,11 +46,11 @@ Git log lists commit history. It allows searching and filtering.
## Git Log Workflow
1. Change to workspace directory
-2. Clone the multi runner projects
-3. Change to project dir
-4. Search by author
-5. Search by date
-6. Combine
+1. Clone the multi runner projects
+1. Change to project dir
+1. Search by author
+1. Search by date
+1. Combine
----------
diff --git a/doc/university/training/topics/merge_conflicts.md b/doc/university/training/topics/merge_conflicts.md
index 9a1ce550868..071baddf508 100644
--- a/doc/university/training/topics/merge_conflicts.md
+++ b/doc/university/training/topics/merge_conflicts.md
@@ -16,15 +16,15 @@ comments: false
## Merge conflicts
1. Checkout a new branch and edit `conflicts.rb`. Add 'Line4' and 'Line5'.
-2. Commit and push
-3. Checkout master and edit `conflicts.rb`. Add 'Line6' and 'Line7' below 'Line3'.
-4. Commit and push to master
-5. Create a merge request and watch it fail
-6. Rebase our new branch with master
-7. Fix conflicts on the `conflicts.rb` file.
-8. Stage the file and continue rebasing
-9. Force push the changes
-10. Finally continue with the Merge Request
+1. Commit and push.
+1. Checkout master and edit `conflicts.rb`. Add 'Line6' and 'Line7' below 'Line3'.
+1. Commit and push to master.
+1. Create a merge request and watch it fail.
+1. Rebase our new branch with master.
+1. Fix conflicts on the `conflicts.rb` file.
+1. Stage the file and continue rebasing.
+1. Force push the changes.
+1. Finally continue with the Merge Request.
----------
diff --git a/doc/university/training/topics/rollback_commits.md b/doc/university/training/topics/rollback_commits.md
index 11cb557651f..44304634f36 100644
--- a/doc/university/training/topics/rollback_commits.md
+++ b/doc/university/training/topics/rollback_commits.md
@@ -41,15 +41,15 @@ comments: false
## Reset Workflow
1. Edit file again 'edit_this_file.rb'
-2. Check status
-3. Add and commit with wrong message
-4. Check log
-5. Amend commit
-6. Check log
-7. Soft reset
-8. Check log
-9. Pull for updates
-10. Push changes
+1. Check status
+1. Add and commit with wrong message
+1. Check log
+1. Amend commit
+1. Check log
+1. Soft reset
+1. Check log
+1. Pull for updates
+1. Push changes
----------
diff --git a/doc/university/training/topics/stash.md b/doc/university/training/topics/stash.md
index 315ced1a196..42eedea14e5 100644
--- a/doc/university/training/topics/stash.md
+++ b/doc/university/training/topics/stash.md
@@ -66,12 +66,12 @@ stashes.
## Git Stash
1. Modify a file
-2. Stage file
-3. Stash it
-4. View our stash list
-5. Confirm no pending changes through status
-5. Apply with pop
-6. View list to confirm changes
+1. Stage file
+1. Stash it
+1. View our stash list
+1. Confirm no pending changes through status
+1. Apply with pop
+1. View list to confirm changes
----------
diff --git a/doc/university/training/topics/tags.md b/doc/university/training/topics/tags.md
index 6333ceedbd7..9526bcbfb82 100644
--- a/doc/university/training/topics/tags.md
+++ b/doc/university/training/topics/tags.md
@@ -22,7 +22,7 @@ comments: false
**Additional resources**
-[http://git-scm.com/book/en/Git-Basics-Tagging](http://git-scm.com/book/en/Git-Basics-Tagging)
+<http://git-scm.com/book/en/Git-Basics-Tagging>
----------
diff --git a/doc/university/training/user_training.md b/doc/university/training/user_training.md
index dccb6cbf071..ca3f777f403 100644
--- a/doc/university/training/user_training.md
+++ b/doc/university/training/user_training.md
@@ -6,91 +6,90 @@ comments: false
---
-# Agenda
+## Agenda
-1. Brief history of Git
-1. GitLab walkthrough
-1. Configure your environment
-1. Workshop
+1. Brief history of Git.
+1. GitLab walkthrough.
+1. Configure your environment.
+1. Workshop.
---
-# Git introduction
+## Git introduction
-https://git-scm.com/about
+<https://git-scm.com/about>
-- Distributed version control
- - Does not rely on connection to a central server
- - Many copies of the complete history
-- Powerful branching and merging
-- Adapts to nearly any workflow
-- Fast, reliable and stable file format
+- Distributed version control.
+ - Does not rely on connection to a central server.
+ - Many copies of the complete history.
+- Powerful branching and merging.
+- Adapts to nearly any workflow.
+- Fast, reliable and stable file format.
---
-# Help!
+## Help!
Use the tools at your disposal when you get stuck.
-- Use '`git help <command>`' command
-- Use Google
-- Read documentation at https://git-scm.com
+- Use '`git help <command>`' command.
+- Use Google.
+- Read documentation at <https://git-scm.com>.
---
-# GitLab Walkthrough
+## GitLab Walkthrough
![fit](logo.png)
---
-# Configure your environment
+## Configure your environment
- Windows: Install 'Git for Windows'
-> https://git-for-windows.github.io
+> <https://git-for-windows.github.io>
- Mac: Type '`git`' in the Terminal application.
> If it's not installed, it will prompt you to install it.
-- Debian: '`sudo apt-get install git-all`'
-or Red Hat '`sudo yum install git-all`'
+- Debian: '`sudo apt-get install git-all`' or Red Hat '`sudo yum install git-all`'
---
-# Git Workshop
+## Git Workshop
-## Overview
+### Overview
-1. Configure Git
-1. Configure SSH Key
-1. Create a project
-1. Committing
-1. Feature branching
-1. Merge requests
-1. Feedback and Collaboration
+1. Configure Git.
+1. Configure SSH Key.
+1. Create a project.
+1. Committing.
+1. Feature branching.
+1. Merge requests.
+1. Feedback and Collaboration.
---
-# Configure Git
+## Configure Git
-One-time configuration of the Git client
+One-time configuration of the Git client:
-```bash
+```sh
git config --global user.name "Your Name"
git config --global user.email you@example.com
```
---
-# Configure SSH Key
+## Configure SSH Key
-```bash
+```sh
ssh-keygen -t rsa -b 4096 -C "you@computer-name"
```
-```bash
+```sh
# You will be prompted for the following information. Press enter to accept the defaults. Defaults appear in parentheses.
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/you/.ssh/id_rsa):
@@ -102,31 +101,30 @@ The key fingerprint is:
39:fc:ce:94:f4:09:13:95:64:9a:65:c1:de:05:4d:01 you@computer-name
```
-Copy your public key and add it to your GitLab profile
+Copy your public key and add it to your GitLab profile:
-```bash
+```sh
cat ~/.ssh/id_rsa.pub
```
-```bash
+```sh
ssh-rsa AAAAB3NzaC1yc2EAAAADAQEL17Ufacg8cDhlQMS5NhV8z3GHZdhCrZbl4gz you@example.com
```
---
-# Create a project
+## Create a project
-- Create a project in your user namespace
- - Choose to import from 'Any Repo by URL' and use
- https://gitlab.com/gitlab-org/training-examples.git
+- Create a project in your user namespace.
+ - Choose to import from 'Any Repo by URL' and use <https://gitlab.com/gitlab-org/training-examples.git>.
- Create a '`development`' or '`workspace`' directory in your home directory.
-- Clone the '`training-examples`' project
+- Clone the '`training-examples`' project.
---
-# Commands
+## Commands (project)
-```
+```sh
mkdir ~/development
cd ~/development
@@ -141,37 +139,37 @@ cd training-examples
---
-# Git concepts
+## Git concepts
-**Untracked files**
+### Untracked files
New files that Git has not been told to track previously.
-**Working area**
+### Working area
Files that have been modified but are not committed.
-**Staging area**
+### Staging area
Modified files that have been marked to go in the next commit.
---
-# Committing
+## Committing
-1. Edit '`edit_this_file.rb`' in '`training-examples`'
-1. See it listed as a changed file (working area)
-1. View the differences
-1. Stage the file
-1. Commit
-1. Push the commit to the remote
-1. View the git log
+1. Edit '`edit_this_file.rb`' in '`training-examples`'.
+1. See it listed as a changed file (working area).
+1. View the differences.
+1. Stage the file.
+1. Commit.
+1. Push the commit to the remote.
+1. View the git log.
---
-# Commands
+## Commands (committing)
-```
+```sh
# Edit `edit_this_file.rb`
git status
git diff
@@ -183,29 +181,29 @@ git log
---
-# Feature branching
+## Feature branching
-- Efficient parallel workflow for teams
-- Develop each feature in a branch
-- Keeps changes isolated
-- Consider a 1-to-1 link to issues
-- Push branches to the server frequently
- - Hint: This is a cheap backup for your work-in-progress code
+- Efficient parallel workflow for teams.
+- Develop each feature in a branch.
+- Keeps changes isolated.
+- Consider a 1-to-1 link to issues.
+- Push branches to the server frequently.
+ - Hint: This is a cheap backup for your work-in-progress code.
---
-# Feature branching
+## Feature branching steps
-1. Create a new feature branch called 'squash_some_bugs'
+1. Create a new feature branch called 'squash_some_bugs'.
1. Edit '`bugs.rb`' and remove all the bugs.
-1. Commit
-1. Push
+1. Commit.
+1. Push.
---
-# Commands
+## Commands (feature branching)
-```
+```sh
git checkout -b squash_some_bugs
# Edit `bugs.rb`
git status
@@ -216,51 +214,50 @@ git push origin squash_some_bugs
---
-# Merge requests
+## Merge requests
-- When you want feedback create a merge request
-- Target is the ‘default’ branch (usually master)
-- Assign or mention the person you would like to review
-- Add 'WIP' to the title if it's a work in progress
-- When accepting, always delete the branch
-- Anyone can comment, not just the assignee
-- Push corrections to the same branch
+- When you want feedback create a merge request.
+- Target is the ‘default’ branch (usually master).
+- Assign or mention the person you would like to review.
+- Add 'WIP' to the title if it's a work in progress.
+- When accepting, always delete the branch.
+- Anyone can comment, not just the assignee.
+- Push corrections to the same branch.
---
-# Merge requests
+## Merge requests steps
-**Create your first merge request**
+Create your first merge request:
-1. Use the blue button in the activity feed
-1. View the diff (changes) and leave a comment
-1. Push a new commit to the same branch
-1. Review the changes again and notice the update
+1. Use the blue button in the activity feed.
+1. View the diff (changes) and leave a comment.
+1. Push a new commit to the same branch.
+1. Review the changes again and notice the update.
---
-# Feedback and Collaboration
+## Feedback and Collaboration
-- Merge requests are a time for feedback and collaboration
-- Giving feedback is hard
-- Be as kind as possible
-- Receiving feedback is hard
-- Be as receptive as possible
-- Feedback is about the best code, not the person. You are not your code
+- Merge requests are a time for feedback and collaboration.
+- Giving feedback is hard.
+- Be as kind as possible.
+- Receiving feedback is hard.
+- Be as receptive as possible.
+- Feedback is about the best code, not the person. You are not your code.
---
-# Feedback and Collaboration
+## Feedback and Collaboration resources
Review the Thoughtbot code-review guide for suggestions to follow when reviewing merge requests:
-[https://github.com/thoughtbot/guides/tree/master/code-review](https://github.com/thoughtbot/guides/tree/master/code-review)
+<https://github.com/thoughtbot/guides/tree/master/code-review>.
-See GitLab merge requests for examples:
-[https://gitlab.com/gitlab-org/gitlab-ce/merge_requests](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests)
+See GitLab merge requests for examples: <https://gitlab.com/gitlab-org/gitlab-ce/merge_requests>.
---
-# Explore GitLab projects
+## Explore GitLab projects
![fit](logo.png)
@@ -274,31 +271,29 @@ See GitLab merge requests for examples:
---
-# Tags
+## Tags
-- Useful for marking deployments and releases
-- Annotated tags are an unchangeable part of Git history
-- Soft/lightweight tags can be set and removed at will
-- Many projects combine an annotated release tag with a stable branch
-- Consider setting deployment/release tags automatically
+- Useful for marking deployments and releases.
+- Annotated tags are an unchangeable part of Git history.
+- Soft/lightweight tags can be set and removed at will.
+- Many projects combine an annotated release tag with a stable branch.
+- Consider setting deployment/release tags automatically.
---
-# Tags
-
-- Create a lightweight tag
-- Create an annotated tag
-- Push the tags to the remote repository
+## Tags steps
-**Additional resources**
+1. Create a lightweight tag.
+1. Create an annotated tag.
+1. Push the tags to the remote repository.
-[http://git-scm.com/book/en/Git-Basics-Tagging](http://git-scm.com/book/en/Git-Basics-Tagging)
+Additional resources: <http://git-scm.com/book/en/Git-Basics-Tagging>.
---
-# Commands
+## Commands (tags)
-```
+```sh
git checkout master
# Lightweight tag
@@ -313,31 +308,31 @@ git push origin --tags
---
-# Merge conflicts
+## Merge conflicts
-- Happen often
-- Learning to fix conflicts is hard
-- Practice makes perfect
+- Happen often.
+- Learning to fix conflicts is hard.
+- Practice makes perfect.
- Force push after fixing conflicts. Be careful!
---
-# Merge conflicts
+## Merge conflicts steps
1. Checkout a new branch and edit `conflicts.rb`. Add 'Line4' and 'Line5'.
-1. Commit and push
+1. Commit and push.
1. Checkout master and edit `conflicts.rb`. Add 'Line6' and 'Line7' below 'Line3'.
-1. Commit and push to master
-1. Create a merge request
+1. Commit and push to master.
+1. Create a merge request.
---
-# Merge conflicts
+## Merge conflicts commands
After creating a merge request you should notice that conflicts exist. Resolve
the conflicts locally by rebasing.
-```
+```sh
git rebase master
# Fix conflicts by editing the files.
@@ -350,7 +345,7 @@ git push origin <branch> -f
---
-# Rebase with squash
+## Rebase with squash
You may end up with a commit log that looks like this:
@@ -368,11 +363,11 @@ Squash these in to meaningful commits using an interactive rebase.
---
-# Rebase with squash
+## Rebase with squash commands
Squash the commits on the same branch we used for the merge conflicts step.
-```
+```sh
git rebase -i master
```
@@ -380,17 +375,17 @@ In the editor, leave the first commit as 'pick' and set others to 'fixup'.
---
-# Questions?
+## Questions?
![fit](logo.png)
Thank you for your hard work!
-**Additional Resources**
+## Additional Resources
-GitLab Documentation [http://docs.gitlab.com](http://docs.gitlab.com/)
-GUI Clients [http://git-scm.com/downloads/guis](http://git-scm.com/downloads/guis)
-Pro git book [http://git-scm.com/book](http://git-scm.com/book)
-Platzi Course [https://courses.platzi.com/courses/git-gitlab/](https://courses.platzi.com/courses/git-gitlab/)
-Code School tutorial [http://try.github.io/](http://try.github.io/)
-Contact Us at `subscribers@gitlab.com`
+- GitLab Documentation: <http://docs.gitlab.com/>.
+- GUI Clients: <http://git-scm.com/downloads/guis>.
+- Pro Git book: <http://git-scm.com/book>.
+- Platzi Course: <https://courses.platzi.com/courses/git-gitlab/>.
+- Code School tutorial: <http://try.github.io/>.
+- Contact us at `subscribers@gitlab.com`.
diff --git a/doc/update/11.4-to-11.5.md b/doc/update/11.4-to-11.5.md
index e64ab2acae2..44105348d14 100644
--- a/doc/update/11.4-to-11.5.md
+++ b/doc/update/11.4-to-11.5.md
@@ -91,11 +91,11 @@ Download and install Go:
# Remove former Go installation folder
sudo rm -rf /usr/local/go
-curl --remote-name --progress https://dl.google.com/go/go1.10.3.linux-amd64.tar.gz
-echo 'fa1b0e45d3b647c252f51f5e1204aba049cde4af177ef9f2181f43004f901035 go1.10.3.linux-amd64.tar.gz' | shasum -a256 -c - && \
- sudo tar -C /usr/local -xzf go1.10.3.linux-amd64.tar.gz
+curl --remote-name --progress https://dl.google.com/go/go1.10.5.linux-amd64.tar.gz
+echo 'a035d9beda8341b645d3f45a1b620cf2d8fb0c5eb409be36b389c0fd384ecc3a go1.10.5.linux-amd64.tar.gz' | shasum -a256 -c - && \
+ sudo tar -C /usr/local -xzf go1.10.5.linux-amd64.tar.gz
sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
-rm go1.10.3.linux-amd64.tar.gz
+rm go1.10.5.linux-amd64.tar.gz
```
### 6. Get latest code
@@ -153,20 +153,6 @@ sudo -u git -H make
### 9. Update Gitaly
-#### New Gitaly configuration options required
-
-In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell`.
-
-```shell
-echo '
-[gitaly-ruby]
-dir = "/home/git/gitaly/ruby"
-
-[gitlab-shell]
-dir = "/home/git/gitlab-shell"
-' | sudo -u git tee -a /home/git/gitaly/config.toml
-```
-
#### Check Gitaly configuration
Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly
@@ -272,10 +258,10 @@ Ensure you're still up-to-date with the latest NGINX configuration changes:
cd /home/git/gitlab
# For HTTPS configurations
-git diff origin/11-1-stable:lib/support/nginx/gitlab-ssl origin/11-5-stable:lib/support/nginx/gitlab-ssl
+git diff origin/11-4-stable:lib/support/nginx/gitlab-ssl origin/11-5-stable:lib/support/nginx/gitlab-ssl
# For HTTP configurations
-git diff origin/11-1-stable:lib/support/nginx/gitlab origin/11-5-stable:lib/support/nginx/gitlab
+git diff origin/11-4-stable:lib/support/nginx/gitlab origin/11-5-stable:lib/support/nginx/gitlab
```
If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx
@@ -309,7 +295,7 @@ There might be new configuration options available for [`gitlab.default.example`
```sh
cd /home/git/gitlab
-git diff origin/11-1-stable:lib/support/init.d/gitlab.default.example origin/11-5-stable:lib/support/init.d/gitlab.default.example
+git diff origin/11-4-stable:lib/support/init.d/gitlab.default.example origin/11-5-stable:lib/support/init.d/gitlab.default.example
```
Ensure you're still up-to-date with the latest init script changes:
diff --git a/doc/update/README.md b/doc/update/README.md
index 7d3c4c310a4..d4fc0cc91bf 100644
--- a/doc/update/README.md
+++ b/doc/update/README.md
@@ -38,12 +38,12 @@ Starting with GitLab 9.1.0 it's possible to upgrade to a newer major, minor, or
patch version of GitLab without having to take your GitLab instance offline.
However, for this to work there are the following requirements:
-1. You can only upgrade 1 minor release at a time. So from 9.1 to 9.2, not to
+- You can only upgrade 1 minor release at a time. So from 9.1 to 9.2, not to
9.3.
-2. You have to use [post-deployment
+- You have to use [post-deployment
migrations](../development/post_deployment_migrations.md) (included in
- zero downtime update steps below)
-3. You are using PostgreSQL. If you are using MySQL please look at the release
+ zero downtime update steps below).
+- You are using PostgreSQL. If you are using MySQL please look at the release
post to see if downtime is required.
Most of the time you can safely upgrade from a patch release to the next minor
diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md
index 6025a5bbcda..d4853a5842e 100644
--- a/doc/user/admin_area/settings/continuous_integration.md
+++ b/doc/user/admin_area/settings/continuous_integration.md
@@ -49,3 +49,19 @@ and the default value is `30 days`. On GitLab.com they
This setting is set per job and can be overridden in
[`.gitlab-ci.yml`](../../../ci/yaml/README.md#artifacts-expire_in).
To disable the expiration, set it to `0`. The default unit is in seconds.
+
+## Archive jobs **[CORE ONLY]**
+
+Archiving jobs is useful for reducing the CI/CD footprint on the system by
+removing some of the capabilities of the jobs (metadata needed to run the job),
+but persisting the traces and artifacts for auditing purposes.
+
+To set the duration for which the jobs will be considered as old and expired:
+
+1. Go to **Admin area > Settings > CI/CD > Continuous Integration and Deployment**.
+1. Change the value of "Archive jobs".
+1. Hit **Save changes** for the changes to take effect.
+
+Once that time passes, the jobs will be archived and no longer able to be
+retried. Make it empty to never expire jobs. It has to be no less than 1 day,
+for example: <code>15 days</code>, <code>1 month</code>, <code>2 years</code>.
diff --git a/doc/user/admin_area/settings/email.md b/doc/user/admin_area/settings/email.md
index 7c9e5bf882e..50c318a4969 100644
--- a/doc/user/admin_area/settings/email.md
+++ b/doc/user/admin_area/settings/email.md
@@ -3,3 +3,20 @@
## Custom logo
The logo in the header of some emails can be customized, see the [logo customization section](../../../customization/branded_page_and_email_header.md).
+
+## Custom hostname for private commit emails
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22560) in GitLab 11.5.
+
+This configuration option sets the email hostname for [private commit emails](../../profile/index.md#private-commit-email),
+and it's, by default, set to `users.noreply.YOUR_CONFIGURED_HOSTNAME`.
+
+In order to change this option:
+
+1. Go to **Admin area > Settings** (`/admin/application_settings`).
+1. Under the **Email** section, change the **Custom hostname (for private commit emails)** field.
+1. Hit **Save** for the changes to take effect.
+
+NOTE: **Note**: Once the hostname gets configured, every private commit email using the previous hostname, will not get
+recognized by GitLab. This can directly conflict with certain [Push rules](https://docs.gitlab.com/ee/push_rules/push_rules.html) such as
+`Check whether author is a GitLab user` and `Check whether committer is the current authenticated user`.
diff --git a/doc/user/admin_area/settings/usage_statistics.md b/doc/user/admin_area/settings/usage_statistics.md
index 35a9d7adb28..bd0155dc712 100644
--- a/doc/user/admin_area/settings/usage_statistics.md
+++ b/doc/user/admin_area/settings/usage_statistics.md
@@ -42,7 +42,11 @@ not send any project names, usernames, or any other specific data. The
information from the usage ping is not anonymous, it is linked to the hostname
of the instance.
-You can view the exact JSON payload in the administration panel.
+You can view the exact JSON payload in the administration panel. To view the payload:
+
+1. Go to the **Admin area** (spanner symbol on the top bar).
+1. Expand **Settings** in the left sidebar and click on **Metrics and profiling**.
+1. Expand **Usage statistics** and click on the **Preview payload** button.
### Deactivate the usage ping
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index f9bdaea185b..6c6119a2691 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -1,36 +1,52 @@
-# Markdown
+# GitLab Markdown
-## GitLab Flavored Markdown (GFM)
-
-> **Note:**
-> Not all of the GitLab-specific extensions to Markdown that are described in
-> this document currently work on our documentation website.
->
-> For the best result, we encourage you to check this document out as rendered
-> by GitLab: [markdown.md]
+This markdown guide is **valid for GitLab's system markdown entries and files**.
+It is not valid for the [GitLab documentation website](https://docs.gitlab.com)
+nor [GitLab's main website](https://about.gitlab.com), as they both use
+[Kramdown](https://kramdown.gettalong.org) as their markdown engine.
+The documentation website uses an extended Kramdown gem, [GitLab Kramdown](https://gitlab.com/gitlab-org/gitlab_kramdown).
+Consult the [GitLab Kramdown Guide](https://about.gitlab.com/handbook/product/technical-writing/markdown-guide/) for a complete Kramdown reference._
-_GitLab uses (as of 11.1) the [CommonMark Ruby Library][commonmarker] for Markdown processing of all new issues, merge requests, comments, and other Markdown content in the GitLab system. As of 11.3, wiki pages and Markdown files (`.md`) in the repositories are also processed with CommonMark. Older content in issues/comments are still processed using the [Redcarpet Ruby library][redcarpet]._
-
-_Where there are significant differences, we will try to call them out in this document._
+## GitLab Flavored Markdown (GFM)
GitLab uses "GitLab Flavored Markdown" (GFM). It extends the [CommonMark specification][commonmark-spec] (which is based on standard Markdown) in a few significant ways to add some useful functionality. It was inspired by [GitHub Flavored Markdown](https://help.github.com/articles/basic-writing-and-formatting-syntax/).
You can use GFM in the following areas:
-- comments
-- issues
-- merge requests
-- milestones
-- snippets (the snippet must be named with a `.md` extension)
-- wiki pages
-- markdown documents inside the repository
+- Comments
+- Issues
+- Merge requests
+- Milestones
+- Snippets (the snippet must be named with a `.md` extension)
+- Wiki pages
+- Markdown documents inside repositories
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.
+dependency to do so. Please see the [`github-markup` gem readme](https://github.com/gitlabhq/markup#markups) for more information.
+
+> **Notes:**
+>
+> For the best result, we encourage you to check this document out as [rendered
+> by GitLab itself](markdown.md).
+>
+> As of 11.1, GitLab uses the [CommonMark Ruby Library][commonmarker] for Markdown
+processing of all new issues, merge requests, comments, and other Markdown content
+in the GitLab system. As of 11.3, wiki pages and Markdown files (`.md`) in the
+repositories are also processed with CommonMark. Older content in issues/comments
+are still processed using the [Redcarpet Ruby library][redcarpet].
+>
+> The documentation website had its [markdown engine migrated from Redcarpet to Kramdown](https://gitlab.com/gitlab-com/gitlab-docs/merge_requests/108)
+in October 2018.
+>
+> _Where there are significant differences, we will try to call them out in this document._
### Transitioning to CommonMark
-You may have Markdown documents in your repository that were written using some of the nuances of RedCarpet's version of Markdown. Since CommonMark uses a slightly stricter syntax, these documents may now display a little strangely since we've transitioned to CommonMark. Numbered lists with nested lists in particular can be displayed incorrectly.
+You may have Markdown documents in your repository that were written using some
+of the nuances of RedCarpet's version of Markdown. Since CommonMark uses a
+slightly stricter syntax, these documents may now display a little strangely
+since we've transitioned to CommonMark. Numbered lists with nested lists in
+particular can be displayed incorrectly.
It is usually quite easy to fix. In the case of a nested list such as this:
@@ -50,11 +66,18 @@ simply add a space to each nested item:
In the documentation below, we try to highlight some of the differences.
-If you have a need to view a document using RedCarpet, you can add the token `legacy_render=1` to the end of the url, like this:
+If you have a need to view a document using RedCarpet, you can add the token
+`legacy_render=1` to the end of the url, like this:
https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md?legacy_render=1
-If you have a large volume of Markdown files, it can be tedious to determine if they will be displayed correctly or not. You can use the [diff_redcarpet_cmark](https://gitlab.com/digitalmoksha/diff_redcarpet_cmark) tool (not an officially supported product) to generate a list of files and differences between how RedCarpet and CommonMark render the files. It can give you a great idea if anything needs to be changed - many times nothing will need to changed.
+If you have a large volume of Markdown files, it can be tedious to determine
+if they will be displayed correctly or not. You can use the
+[diff_redcarpet_cmark](https://gitlab.com/digitalmoksha/diff_redcarpet_cmark)
+tool (not an officially supported product) to generate a list of files and
+differences between how RedCarpet and CommonMark render the files. It can give
+you a great idea if anything needs to be changed - many times nothing will need
+to changed.
### Newlines
@@ -63,7 +86,8 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#newline
GFM honors the markdown specification in how [paragraphs and line breaks are handled][commonmark-spec].
-A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines.
+A paragraph is simply one or more consecutive lines of text, separated by one or
+more blank lines.
Line-breaks, or soft returns, are rendered if you end a line with two or more spaces:
<!-- (Do *NOT* remove the two ending whitespaces in the following line.) -->
@@ -85,7 +109,9 @@ Sugar is sweet
> If this is not rendered correctly, see
https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#multiple-underscores-in-words
-It is not reasonable to italicize just _part_ of a word, especially when you're dealing with code and names that often appear with multiple underscores. Therefore, GFM ignores multiple underscores in words:
+It is not reasonable to italicize just _part_ of a word, especially when you're
+dealing with code and names that often appear with multiple underscores.
+Therefore, GFM ignores multiple underscores in words:
perform_complicated_task
@@ -124,7 +150,7 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#multili
On top of standard Markdown [blockquotes](#blockquotes), which require prepending `>` to quoted lines,
GFM supports multiline blockquotes fenced by <code>>>></code>:
-```no-highlight
+```
>>>
If you paste a message from somewhere else
@@ -158,7 +184,7 @@ Blocks of code are either fenced by lines with three back-ticks <code>```</code>
or are indented with four spaces. Only the fenced code blocks support syntax
highlighting:
-```no-highlight
+```
Inline `code` has `back-ticks around` it.
```
@@ -248,21 +274,23 @@ However the wrapping tags cannot be mixed as such:
> If this is not rendered correctly, see
https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#emoji
- Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you:
+```
+Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you:
- :zap: You can use emoji anywhere GFM is supported. :v:
+:zap: You can use emoji anywhere GFM is supported. :v:
- You can use it to point out a :bug: or warn about :speak_no_evil: patches. And if someone improves your really :snail: code, send them some :birthday:. People will :heart: you for that.
+You can use it to point out a :bug: or warn about :speak_no_evil: patches. And if someone improves your really :snail: code, send them some :birthday:. People will :heart: you for that.
- If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up one of the supported codes.
+If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up one of the supported codes.
- Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup:
+Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup:
- Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support.
+Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support.
- On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/help/emoji/) to get full native emoji support.
+On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/help/emoji/) to get full native emoji support.
- Ubuntu 18.04 (like many modern Linux distros) has this font installed by default.
+Ubuntu 18.04 (like many modern Linux distros) has this font installed by default.
+```
Sometimes you want to <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/monkey.png" width="20px" height="20px"> around a bit and add some <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/star2.png" width="20px" height="20px"> to your <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/speech_balloon.png" width="20px" height="20px">. Well we have a gift for you:
@@ -281,7 +309,6 @@ On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/he
Ubuntu 18.04 (like many modern Linux distros) has this font installed by default.
-
### Special GitLab References
GFM recognizes special references.
@@ -343,7 +370,7 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#task-li
You can add task lists to issues, merge requests and comments. To create a task list, add a specially-formatted Markdown list, like so:
-```no-highlight
+```
- [x] Completed task
- [ ] Incomplete task
- [ ] Sub-task 1
@@ -355,7 +382,7 @@ You can add task lists to issues, merge requests and comments. To create a task
Tasks formatted as ordered lists are supported as well:
-```no-highlight
+```
1. [x] Completed task
1. [ ] Incomplete task
1. [ ] Sub-task 1
@@ -412,7 +439,7 @@ This math is inline ![alt text](img/math_inline_sup_render_gfm.png).
This is on a separate line
-<div align="center"><img src="./img/math_inline_sup_render_gfm.png" ></div>
+<img src="./img/math_inline_sup_render_gfm.png" >
_Be advised that KaTeX only supports a [subset][katex-subset] of LaTeX._
@@ -440,7 +467,7 @@ Examples:
`HSL(540,70%,50%)`
`HSLA(540,70%,50%,0.7)`
-Become:
+Becomes:
![alt color-inline-colorchip-render-gfm](img/color_inline_colorchip_render_gfm.png)
@@ -482,7 +509,7 @@ For details see the [Mermaid official page][mermaid].
### Headers
-```no-highlight
+```
# H1
## H2
### H3
@@ -540,7 +567,7 @@ Note that the Emoji processing happens before the header IDs are generated, so t
Examples:
-```no-highlight
+```
Emphasis, aka italics, with *asterisks* or _underscores_.
Strong emphasis, aka bold, with **asterisks** or __underscores__.
@@ -550,7 +577,7 @@ Combined emphasis with **asterisks and _underscores_**.
Strikethrough uses two tildes. ~~Scratch this.~~
```
-Become:
+Becomes:
Emphasis, aka italics, with *asterisks* or _underscores_.
@@ -564,7 +591,7 @@ Strikethrough uses two tildes. ~~Scratch this.~~
Examples:
-```no-highlight
+```
1. First ordered list item
2. Another item
* Unordered sub-list.
@@ -577,7 +604,7 @@ Examples:
+ Or pluses
```
-Become:
+Becomes:
1. First ordered list item
2. Another item
@@ -595,7 +622,7 @@ each subsequent paragraph should be indented to the same level as the start of t
Example:
-```no-highlight
+```
1. First ordered list item
Second paragraph of first item.
@@ -616,7 +643,7 @@ the paragraph will appear outside the list, instead of properly indented under t
Example:
-```no-highlight
+```
1. First ordered list item
Paragraph of first item.
@@ -676,7 +703,7 @@ Examples:
[logo]: img/markdown_logo.png
-Become:
+Becomes:
Here's our logo:
@@ -694,7 +721,7 @@ Reference-style:
Examples:
-```no-highlight
+```
> Blockquotes are very handy in email to emulate reply text.
> This line is part of the same quote.
@@ -703,7 +730,7 @@ Quote break.
> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.
```
-Become:
+Becomes:
> Blockquotes are very handy in email to emulate reply text.
> This line is part of the same quote.
@@ -720,7 +747,7 @@ See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubyd
Examples:
-```no-highlight
+```
<dl>
<dt>Definition list</dt>
<dd>Is something people use sometimes.</dd>
@@ -730,7 +757,7 @@ Examples:
</dl>
```
-Become:
+Becomes:
<dl>
<dt>Definition list</dt>
@@ -755,7 +782,7 @@ These details <em>will</em> remain <strong>hidden</strong> until expanded.
</details>
</p>
-**Note:** Markdown inside these tags is supported, as long as you have a blank link after the `</summary>` tag and before the `</details>` tag, as shown in the example. _Redcarpet does not support Markdown inside these tags. You can work around this by using HTML, for example you can use `<pre><code>` tags instead of [code fences](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#code-and-syntax-highlighting)._
+**Note:** Markdown inside these tags is supported, as long as you have a blank line after the `</summary>` tag and before the `</details>` tag, as shown in the example. _Redcarpet does not support Markdown inside these tags. You can work around this by using HTML, for example you can use `<pre><code>` tags instead of [code fences](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#code-and-syntax-highlighting)._
```html
<details>
@@ -788,7 +815,7 @@ ___
Underscores
```
-Become:
+Becomes:
Three or more...
@@ -826,7 +853,7 @@ This line is *on its own line*, because the previous line ends with two spaces.
spaces.
```
-Become:
+Becomes:
Here's a line for us to start with.
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 4359592905d..1fd230a41aa 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -95,6 +95,7 @@ The following table depicts the various user permission levels in a project.
| Manage GitLab Pages | | | | ✓ | ✓ |
| Manage GitLab Pages domains and certificates | | | | ✓ | ✓ |
| Remove GitLab Pages | | | | | ✓ |
+| View GitLab Pages protected by [access control](../administration/pages/index.md#access-control) | ✓ | ✓ | ✓ | ✓ | ✓ |
| Manage clusters | | | | ✓ | ✓ |
| Manage license policy **[ULTIMATE]** | | | | ✓ | ✓ |
| Edit comments (posted by any user) | | | | ✓ | ✓ |
@@ -206,7 +207,7 @@ They will, like usual users, receive a role in the project or group with all
the abilities that are mentioned in the table above. They cannot however create
groups or projects, and they have the same access as logged out users in all
other cases.
-
+
An administrator can flag a user as external [through the API](../api/users.md)
or by checking the checkbox on the admin panel. As an administrator, navigate
to **Admin > Users** to create a new user or edit an existing one. There, you
@@ -217,7 +218,7 @@ by an administrator under **Admin > Application Settings**.
### Default internal users
-The "Internal users" field allows specifying an e-mail address regex pattern to identify default internal users.
+The "Internal users" field allows specifying an e-mail address regex pattern to identify default internal users.
New users whose email address matches the regex pattern will be set to internal by default rather than an external collaborator.
diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md
index 64219737d61..76f7e869ff7 100644
--- a/doc/user/profile/account/two_factor_authentication.md
+++ b/doc/user/profile/account/two_factor_authentication.md
@@ -158,7 +158,7 @@ authentication. If an SSH key is added to your GitLab account, you can generate
a new set of recovery codes with SSH.
1. Run `ssh git@gitlab.example.com 2fa_recovery_codes`.
-2. You are prompted to confirm that you want to generate new codes. Continuing this process invalidates previously saved codes.
+1. You are prompted to confirm that you want to generate new codes. Continuing this process invalidates previously saved codes.
```
bash
$ ssh git@gitlab.example.com 2fa_recovery_codes
@@ -185,7 +185,7 @@ a new set of recovery codes with SSH.
so you do not lose access to your account again.
```
-3. Go to the GitLab sign-in page and enter your username/email and password.
+1. Go to the GitLab sign-in page and enter your username/email and password.
When prompted for a two-factor code, enter one of the recovery codes obtained
from the command-line output.
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index ab62762f343..2f989a26725 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -31,6 +31,7 @@ From there, you can:
- Update your personal information
- Set a [custom status](#current-status) for your profile
+- Manage your [commit email](#commit-email) for your profile
- Manage [2FA](account/two_factor_authentication.md)
- Change your username and [delete your account](account/delete_account.md)
- Manage applications that can
@@ -96,13 +97,13 @@ You and GitLab admins can see your the abovementioned information on your profil
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/14078) in GitLab 11.3.
-Enabling private contributions will include contributions to private projects, in the user contribution calendar graph and user recent activity.
+Enabling private contributions will include contributions to private projects, in the user contribution calendar graph and user recent activity.
To enable private contributions:
1. Navigate to your personal [profile settings](#profile-settings).
-2. Check the "Private contributions" option.
-3. Hit **Update profile settings**.
+1. Check the "Private contributions" option.
+1. Hit **Update profile settings**.
## Current status
@@ -132,6 +133,45 @@ They may however contain emoji codes such as `I'm on vacation :palm_tree:`.
You can also set your current status [using the API](../../api/users.md#user-status).
+## Commit email
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21598) in GitLab 11.4.
+
+A commit email, is the email that will be displayed in every Git-related action done through the
+GitLab interface.
+
+You are able to select from the list of your own verified emails which email you want to use as the commit email.
+
+To change it:
+
+1. Open the user menu in the top-right corner of the navigation bar.
+1. Hit **Commit email** selection box.
+1. Select any of the verified emails.
+1. Hit **Update profile settings**.
+
+### Private commit email
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22560) in GitLab 11.5.
+
+GitLab provides the user with an automatically generated private commit email option,
+which allows the user to not make their email information public.
+
+To enable this option:
+
+1. Open the user menu in the top-right corner of the navigation bar.
+1. Hit **Commit email** selection box.
+1. Select **Use a private email** option.
+1. Hit **Update profile settings**.
+
+Once this option is enabled, every Git-related action will be performed using the private commit email.
+
+In order to stay fully annonymous, you can also copy this private commit email
+and configure it on your local machine using the following command:
+
+```
+git config --global user.email "YOUR_PRIVATE_COMMIT_EMAIL"
+```
+
## Troubleshooting
### Why do I keep getting signed out?
diff --git a/doc/user/project/clusters/eks_and_gitlab/img/rbac.png b/doc/user/project/clusters/eks_and_gitlab/img/rbac.png
new file mode 100644
index 00000000000..c8adaad13c2
--- /dev/null
+++ b/doc/user/project/clusters/eks_and_gitlab/img/rbac.png
Binary files differ
diff --git a/doc/user/project/clusters/eks_and_gitlab/index.md b/doc/user/project/clusters/eks_and_gitlab/index.md
index 10f0cdb333e..45d77e075f1 100644
--- a/doc/user/project/clusters/eks_and_gitlab/index.md
+++ b/doc/user/project/clusters/eks_and_gitlab/index.md
@@ -1,25 +1,19 @@
----
-author: Joshua Lambert
-author_gitlab: joshlambert
-level: intermediate
-article_type: tutorial
-date: 2018-06-05
----
-
# Connecting and deploying to an Amazon EKS cluster
## Introduction
-In this tutorial, we will show how easy it is to integrate an [Amazon EKS](https://aws.amazon.com/eks/) cluster with GitLab, and begin deploying applications.
+In this tutorial, we will show how to integrate an [Amazon EKS](https://aws.amazon.com/eks/) cluster with GitLab, and begin deploying applications.
For an end-to-end walkthrough we will:
+
1. Start with a new project based on the sample Ruby on Rails template
1. Integrate an EKS cluster
1. Utilize [Auto DevOps](../../../../topics/autodevops/) to build, test, and deploy our application
You will need:
+
1. An account on GitLab, like [GitLab.com](https://gitlab.com)
-1. An Amazon EKS cluster
+1. An Amazon EKS cluster (with worker nodes properly configured)
1. `kubectl` [installed and configured for access to the EKS cluster](https://docs.aws.amazon.com/eks/latest/userguide/getting-started.html#get-started-kubectl)
If you don't have an Amazon EKS cluster, one can be created by following [the EKS getting started guide](https://docs.aws.amazon.com/eks/latest/userguide/getting-started.html).
@@ -36,25 +30,103 @@ Give the project a name, and then select `Create project`.
![Create Project](img/create_project.png)
-## Connecting the EKS cluster
+## Configuring and connecting the EKS cluster
From the left side bar, hover over `Operations` and select `Kubernetes`, then click on `Add Kubernetes cluster`, and finally `Add an existing Kubernetes cluster`.
A few details from the EKS cluster will be required to connect it to GitLab.
-1. A valid Kubernetes certificate and token are needed to authenticate to the EKS cluster. A pair is created by default, which can be used. Open a shell and use `kubectl` to retrieve them:
- * List the secrets with `kubectl get secrets`, and one should named similar to `default-token-xxxxx`. Copy that token name for use below.
- * Get the certificate with `kubectl get secret <secret name> -o jsonpath="{['data']['ca\.crt']}" | base64 -D`
- * Retrieve the token with `kubectl get secret <secret name> -o jsonpath="{['data']['token']}" | base64 -D`.
+1. **Retrieve the certificate**: A valid Kubernetes certificate is needed to authenticate to the EKS cluster. We will use the certificate created by default. Open a shell and use `kubectl` to retrieve it:
+ - List the secrets with `kubectl get secrets`, and one should named similar to `default-token-xxxxx`. Copy that token name for use below.
+ - Get the certificate with `kubectl get secret <secret name> -o jsonpath="{['data']['ca\.crt']}" | base64 -D`
+
+1. **Create admin token**: A `cluster-admin` token is required to install and manage Helm Tiller. GitLab establishes mutual SSL auth with Helm Tiller and creates limited service accounts for each application. To create the token we will create an admin service account as follows:
+
+ 1. Create a file called `eks-admin-service-account.yaml` with the text below:
+
+ ```yaml
+ apiVersion: v1
+ kind: ServiceAccount
+ metadata:
+ name: eks-admin
+ namespace: kube-system
+ ```
+
+ 2. Apply the service account to your cluster:
+
+ ```bash
+ kubectl apply -f eks-admin-service-account.yaml
+ ```
+
+ Output:
+
+ ```bash
+ serviceaccount "eks-admin" created
+ ```
+
+ 3. Create a file called `eks-admin-cluster-role-binding.yaml` with the text below:
+
+ ```yaml
+ apiVersion: rbac.authorization.k8s.io/v1beta1
+ kind: ClusterRoleBinding
+ metadata:
+ name: eks-admin
+ roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: cluster-admin
+ subjects:
+ - kind: ServiceAccount
+ name: eks-admin
+ namespace: kube-system
+ ```
+
+ 4. Apply the cluster role binding to your cluster:
+
+ ```bash
+ kubectl apply -f eks-admin-cluster-role-binding.yaml
+ ```
+
+ Output:
+
+ ```bash
+ clusterrolebinding "eks-admin" created
+ ```
+
+ 5. Retrieve the token for the `eks-admin` service account. Copy the `<authentication_token>` value from the output.
+
+ ```bash
+ kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep eks-admin | awk '{print $1}')
+ ```
+
+ Output:
+
+ ```yaml
+ Name: eks-admin-token-b5zv4
+ Namespace: kube-system
+ Labels: <none>
+ Annotations: kubernetes.io/service-account.name=eks-admin
+ kubernetes.io/service-account.uid=bcfe66ac-39be-11e8-97e8-026dce96b6e8
+
+ Type: kubernetes.io/service-account-token
+
+ Data
+ ====
+ ca.crt: 1025 bytes
+ namespace: 11 bytes
+ token: <authentication_token>
+ ```
+
1. The API server endpoint is also required, so GitLab can connect to the cluster. This is displayed on the AWS EKS console, when viewing the EKS cluster details.
You now have all the information needed to connect the EKS cluster:
-* Kubernetes cluster name: Provide a name for the cluster to identify it within GitLab.
-* Environment scope: Leave this as `*` for now, since we are only connecting a single cluster.
-* API URL: Paste in the API server endpoint retrieved above.
-* CA Certificate: Paste the certificate data from the earlier step, as-is.
-* Paste the token value. Note on some versions of Kubernetes a trailing `%` is output, do not include it.
-* Project namespace: This can be left blank to accept the default namespace, based on the project name.
+
+- Kubernetes cluster name: Provide a name for the cluster to identify it within GitLab.
+- Environment scope: Leave this as `*` for now, since we are only connecting a single cluster.
+- API URL: Paste in the API server endpoint retrieved above.
+- CA Certificate: Paste the certificate data from the earlier step, as-is.
+- Paste the admin token value.
+- Project namespace: This can be left blank to accept the default namespace, based on the project name.
![Add Cluster](img/add_cluster.png)
@@ -62,9 +134,11 @@ Click on `Add Kubernetes cluster`, the cluster is now connected to GitLab. At th
If you would like to utilize your own CI/CD scripts to deploy to the cluster, you can stop here.
-## Disable Role-Based Access Control (RBAC)
+## Disable Role-Based Access Control (RBAC) - Optional
+
+When connecting a cluster via GitLab integration, you may specify whether the cluster is RBAC-enabled or not. This will affect how GitLab interacts with the cluster for certain operations. If you **did not** check the "RBAC-enabled cluster" checkbox at creation time, GitLab will assume RBAC is disabled for your cluster when interacting with it. If so, you must disable RBAC on your cluster for the integration to work properly.
-Presently, Auto DevOps and one-click app installs do not support [Kubernetes role-based access control](https://kubernetes.io/docs/reference/access-authn-authz/rbac/). Support is [being worked on](https://gitlab.com/groups/gitlab-org/-/epics/136), but in the interim RBAC must be disabled to utilize for these features.
+![rbac](img/rbac.png)
> **Note**: Disabling RBAC means that any application running in the cluster, or user who can authenticate to the cluster, has full API access. This is a [security concern](https://docs.gitlab.com/ee/user/project/clusters/#security-implications), and may not be desirable.
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index 48004471f0a..3fbd4c21eab 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -113,6 +113,14 @@ To add an existing Kubernetes cluster to your project:
After a couple of minutes, your cluster will be ready to go. You can now proceed
to install some [pre-defined applications](#installing-applications).
+To determine the:
+
+- API URL, run `kubectl cluster-info | grep 'Kubernetes master' | awk '/http/ {print $NF}'`.
+- Token:
+ 1. List the secrets by running: `kubectl get secrets`. Note the name of the secret you need the token for.
+ 1. Get the token for the appropriate secret by running: `kubectl get secret <SECRET_NAME> -o jsonpath="{['data']['token']}" | base64 -D`.
+- CA certificate, run `kubectl get secret <secret name> -o jsonpath="{['data']['ca\.crt']}" | base64 -D`.
+
## Security implications
CAUTION: **Important:**
@@ -124,59 +132,63 @@ functionalities needed to successfully build and deploy a containerized
application. Bare in mind that the same credentials are used for all the
applications running on the cluster.
-When GitLab creates the cluster, it enables and uses the legacy
-[Attribute-based access control (ABAC)](https://kubernetes.io/docs/admin/authorization/abac/).
-The newer [RBAC](https://kubernetes.io/docs/admin/authorization/rbac/)
-authorization is [experimental](#role-based-access-control-rbac).
+## Access controls
-### Role-based access control (RBAC) **[CORE ONLY]**
+When creating a cluster in GitLab, you will be asked if you would like to create an
+[Attribute-based access control (ABAC)](https://kubernetes.io/docs/admin/authorization/abac/) cluster, or
+a [Role-based access control (RBAC)](https://kubernetes.io/docs/admin/authorization/rbac/) one.
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21401) in GitLab 11.4.
+Whether ABAC or RBAC is enabled, GitLab will create the necessary
+service accounts and privileges in order to install and run
+[GitLab managed applications](#installing-applications):
-CAUTION: **Warning:**
-The RBAC authorization is experimental.
+- A `gitlab` service account with `cluster-admin` privileges will be created in the
+ `default` namespace, which will be used by GitLab to manage the newly created cluster.
-Once RBAC is enabled for a cluster, GitLab will create the necessary service accounts
-and privileges in order to install and run [GitLab managed applications](#installing-applications).
+- A project service account with [`edit`
+ privileges](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles)
+ will be created in the project namespace (also created by GitLab), which will
+ be used in [deployment jobs](#deployment-variables).
-If you are creating a [new GKE cluster via
-GitLab](#adding-and-creating-a-new-gke-cluster-via-gitlab), you will be
-asked if you would like to create an RBAC-enabled cluster. Enabling this
-setting will create a `gitlab` service account which will be used by
-GitLab to manage the newly created cluster. To enable this, this service
-account will have the `cluster-admin` privilege.
+ NOTE: **Note:**
+ Restricted service account for deployment was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/51716) in GitLab 11.5.
-If you are [adding an existing Kubernetes
-cluster](#adding-an-existing-kubernetes-cluster), you will be asked if
-the cluster you are adding is a RBAC-enabled cluster. Ensure the
-token of the account has administrator privileges for the cluster.
+- When you install Helm Tiller 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
+ installed application. Consult the documentation of the Helm charts for each application
+ for details.
-In both cases above, when you install Helm Tiller into your cluster, an
-RBAC-enabled cluster will create a `tiller` service account, 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).
+If you are [adding an existing Kubernetes cluster](#adding-an-existing-kubernetes-cluster),
+ensure the token of the account has administrator privileges for the cluster.
-The table below summarizes which resources will be created in a
-RBAC-enabled cluster :
+The following sections summarize which resources will be created on ABAC/RBAC clusters.
-| Name | Kind | Details | Created when |
-| --- | --- | --- | --- |
-| `gitlab` | `ServiceAccount` | `default` namespace | Creating a new GKE Cluster |
-| `gitlab-admin` | `ClusterRoleBinding` | `cluster-admin` roleRef | Creating a new GKE Cluster |
-| `gitlab-token` | `Secret` | Token for `gitlab` ServiceAccount | Creating a new GKE Cluster |
-| `tiller` | `ServiceAccount` | `gitlab-managed-apps` namespace | Installing Helm Tiller |
-| `tiller-admin` | `ClusterRoleBinding` | `cluster-admin` roleRef | Installing Helm Tiller |
+### Attribute-based access control (ABAC)
+| Name | Kind | Details | Created when |
+| --- | --- | --- | --- |
+| `gitlab` | `ServiceAccount` | `default` namespace | Creating a new GKE Cluster |
+| `gitlab-token` | `Secret` | Token for `gitlab` ServiceAccount | Creating a new GKE Cluster |
+| `tiller` | `ServiceAccount` | `gitlab-managed-apps` namespace | Installing Helm Tiller |
+| `tiller-admin` | `ClusterRoleBinding` | `cluster-admin` roleRef | Installing Helm Tiller |
+| 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 |
-Helm Tiller will also create additional service accounts and other RBAC
-resources for each installed application. Consult the documentation for the
-Helm charts for each application for details.
+### Role-based access control (RBAC)
-NOTE: **Note:**
-Auto DevOps will not successfully complete in a cluster that only has RBAC
-authorization enabled. RBAC support for Auto DevOps is planned in a
-[future release](https://gitlab.com/gitlab-org/gitlab-ce/issues/44597).
+| Name | Kind | Details | Created when |
+| --- | --- | --- | --- |
+| `gitlab` | `ServiceAccount` | `default` namespace | Creating a new GKE Cluster |
+| `gitlab-admin` | `ClusterRoleBinding` | [`cluster-admin`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles) roleRef | Creating a new GKE Cluster |
+| `gitlab-token` | `Secret` | Token for `gitlab` ServiceAccount | Creating a new GKE Cluster |
+| `tiller` | `ServiceAccount` | `gitlab-managed-apps` namespace | Installing Helm Tiller |
+| `tiller-admin` | `ClusterRoleBinding` | `cluster-admin` roleRef | Installing Helm Tiller |
+| 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 |
### Security of GitLab Runners
@@ -203,7 +215,7 @@ added directly to your configured cluster. Those applications are needed for
[Review Apps](../../../ci/review_apps/index.md) and [deployments](../../../ci/environments.md).
NOTE: **Note:**
-The applications will be installed in a dedicated namespace called
+With the exception of Knative, the applications will be installed in a dedicated namespace called
`gitlab-managed-apps`. In case you have added an existing Kubernetes cluster
with Tiller already installed, you should be careful as GitLab cannot
detect it. By installing it via the applications will result into having it
@@ -216,6 +228,7 @@ twice, which can lead to confusion during deployments.
| [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) |
| [GitLab Runner](https://docs.gitlab.com/runner/) | 10.6+ | 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](https://about.gitlab.com/features/gitlab-ci-cd/), 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](#security-implications) before doing so. | [runner/gitlab-runner](https://gitlab.com/charts/gitlab-runner) |
| [JupyterHub](http://jupyter.org/) | 11.0+ | [JupyterHub](https://jupyterhub.readthedocs.io/en/stable/) is a multi-user service for managing notebooks across a team. [Jupyter Notebooks](https://jupyter-notebook.readthedocs.io/en/latest/) provide a web-based interactive programming environment used for data analysis, visualization, and machine learning. We use [this](https://gitlab.com/gitlab-org/jupyterhub-user-image/blob/master/Dockerfile) custom Jupyter image that installs additional useful packages on top of the base Jupyter. You will also see ready-to-use DevOps Runbooks built with Nurtch's [Rubix library](https://github.com/amit1rrr/rubix). More information on creating executable runbooks can be found at [Nurtch Documentation](http://docs.nurtch.com/en/latest). **Note**: Authentication will be enabled for any user of the GitLab server via OAuth2. HTTPS will be supported in a future release. | [jupyter/jupyterhub](https://jupyterhub.github.io/helm-chart/) |
+| [Knative](https://cloud.google.com/knative) | 0.1.2 | Knative provides a platform to create, deploy, and manage serverless workloads from a Kubernetes cluster. It is used in conjunction with, and includes [Istio](https://istio.io) to provide an external IP address for all programs hosted by Knative. You will be prompted to enter a wildcard domain where your applications will be exposed. Configure your DNS server to use the external IP address for that domain. For any application created and installed, they will be accessible as <program_name>.<kubernetes_namespace>.<domain_name>. **Note**: This will require your kubernetes cluster to have RBAC enabled. | [knative/knative](https://storage.googleapis.com/triggermesh-charts)
## Getting the external IP address
@@ -224,6 +237,10 @@ You need a load balancer installed in your cluster in order to obtain the
external IP address with the following procedure. It can be deployed using the
[**Ingress** application](#installing-applications).
+NOTE: **Note:**
+Knative will include its own load balancer in the form of [Istio](https://istio.io).
+At this time, to determine the external IP address, you will need to follow the manual approach.
+
In order to publish your web application, you first need to find the external IP
address associated to your load balancer.
@@ -254,6 +271,12 @@ run the following command:
kubectl get svc --namespace=gitlab-managed-apps ingress-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip} '
```
+NOTE: **Note:**
+For Istio/Knative, the command will be different:
+```bash
+kubectl get svc --namespace=istio-system knative-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip} '
+```
+
Otherwise, you can list the IP addresses of all load balancers:
```bash
@@ -368,12 +391,16 @@ GitLab CI/CD build environment.
| Variable | Description |
| -------- | ----------- |
| `KUBE_URL` | Equal to the API URL. |
-| `KUBE_TOKEN` | The Kubernetes token. |
+| `KUBE_TOKEN` | The Kubernetes token of the [project service account](#access-controls). |
| `KUBE_NAMESPACE` | The Kubernetes namespace is auto-generated if not specified. The default value is `<project_name>-<project_id>`. You can overwrite it to use different one if needed, otherwise the `KUBE_NAMESPACE` variable will receive the default value. |
-| `KUBE_CA_PEM_FILE` | Only present if a custom CA bundle was specified. Path to a file containing PEM data. |
-| `KUBE_CA_PEM` | (**deprecated**) Only if a custom CA bundle was specified. Raw PEM data. |
+| `KUBE_CA_PEM_FILE` | Path to a file containing PEM data. Only present if a custom CA bundle was specified. |
+| `KUBE_CA_PEM` | (**deprecated**) Raw PEM data. Only if a custom CA bundle was specified. |
| `KUBECONFIG` | Path to a file containing `kubeconfig` for this deployment. CA bundle would be embedded if specified. |
+NOTE: **NOTE:**
+Prior to GitLab 11.5, `KUBE_TOKEN` was the Kubernetes token of the main
+service account of the cluster integration.
+
## Enabling or disabling the Kubernetes cluster integration
After you have successfully added your cluster information, you can enable the
diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md
index 1b1827a2658..cac64fc0cb6 100644
--- a/doc/user/project/container_registry.md
+++ b/doc/user/project/container_registry.md
@@ -139,12 +139,12 @@ docker login registry.example.com -u <username> -p <token>
1. Check to make sure that the system clock on your Docker client and GitLab server have
been synchronized (e.g. via NTP).
-2. If you are using an S3-backed Registry, double check that the IAM
+1. If you are using an S3-backed Registry, double check that the IAM
permissions and the S3 credentials (including region) are correct. See [the
sample IAM policy](https://docs.docker.com/registry/storage-drivers/s3/)
for more details.
-3. Check the Registry logs (e.g. `/var/log/gitlab/registry/current`) and the GitLab production logs
+1. Check the Registry logs (e.g. `/var/log/gitlab/registry/current`) and the GitLab production logs
for errors (e.g. `/var/log/gitlab/gitlab-rails/production.log`). You may be able to find clues
there.
diff --git a/doc/user/project/deploy_tokens/index.md b/doc/user/project/deploy_tokens/index.md
index dc73194309c..7688508c6ac 100644
--- a/doc/user/project/deploy_tokens/index.md
+++ b/doc/user/project/deploy_tokens/index.md
@@ -13,8 +13,8 @@ You can create as many deploy tokens as you like from the settings of your proje
1. Log in to your GitLab account.
1. Go to the project you want to create Deploy Tokens for.
-1. Go to **Settings** > **Repository**
-1. Click on "Expand" on **Deploy Tokens** section
+1. Go to **Settings** > **Repository**.
+1. Click on "Expand" on **Deploy Tokens** section.
1. Choose a name and optionally an expiry date for the token.
1. Choose the [desired scopes](#limiting-scopes-of-a-deploy-token).
1. Click on **Create deploy token**.
@@ -46,8 +46,8 @@ the following table.
To download a repository using a Deploy Token, you just need to:
1. Create a Deploy Token with `read_repository` as a scope.
-2. Take note of your `username` and `token`
-3. `git clone` the project using the Deploy Token:
+1. Take note of your `username` and `token`.
+1. `git clone` the project using the Deploy Token:
```sh
git clone http://<username>:<deploy_token>@gitlab.example.com/tanuki/awesome_project.git
@@ -60,8 +60,8 @@ Replace `<username>` and `<deploy_token>` with the proper values.
To read the container registry images, you'll need to:
1. Create a Deploy Token with `read_registry` as a scope.
-2. Take note of your `username` and `token`
-3. Log in to GitLab’s Container Registry using the deploy token:
+1. Take note of your `username` and `token`.
+1. Log in to GitLab’s Container Registry using the deploy token:
```sh
docker login registry.example.com -u <username> -p <deploy_token>
diff --git a/doc/user/project/import/clearcase.md b/doc/user/project/import/clearcase.md
index f23623ed485..89a9f7da852 100644
--- a/doc/user/project/import/clearcase.md
+++ b/doc/user/project/import/clearcase.md
@@ -1,6 +1,6 @@
# Migrating from ClearCase
-[ClearCase](https://www-03.ibm.com/software/products/en/clearcase/) is a set of
+[ClearCase](https://www.ibm.com/us-en/marketplace/rational-clearcase) is a set of
tools developed by IBM which also include a centralized version control system
similar to Git.
diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md
index fcd6192e82f..3e4be043199 100644
--- a/doc/user/project/import/github.md
+++ b/doc/user/project/import/github.md
@@ -65,9 +65,9 @@ developer documentation.
Before you begin, ensure that any GitHub users who you want to map to GitLab users have either:
-1. A GitLab account that has logged in using the GitHub icon
+- A GitLab account that has logged in using the GitHub icon
\- or -
-2. A GitLab account with an email address that matches the [public email address](https://help.github.com/articles/setting-your-commit-email-address-on-github/) of the GitHub user
+- A GitLab account with an email address that matches the [public email address](https://help.github.com/articles/setting-your-commit-email-address-on-github/) of the GitHub user
User-matching attempts occur in that order, and if a user is not identified either way, the activity is associated with
the user account that is performing the import.
@@ -77,10 +77,10 @@ If you are using a self-hosted GitLab instance, this process requires that you h
[GitHub integration][gh-import].
1. From the top navigation bar, click **+** and select **New project**.
-2. Select the **Import project** tab and then select **GitHub**.
-3. Select the first button to **List your GitHub repositories**. You are redirected to a page on github.com to authorize the GitLab application.
-4. Click **Authorize gitlabhq**. You are redirected back to GitLab's Import page and all of your GitHub repositories are listed.
-5. Continue on to [selecting which repositories to import](#selecting-which-repositories-to-import).
+1. Select the **Import project** tab and then select **GitHub**.
+1. Select the first button to **List your GitHub repositories**. You are redirected to a page on github.com to authorize the GitLab application.
+1. Click **Authorize gitlabhq**. You are redirected back to GitLab's Import page and all of your GitHub repositories are listed.
+1. Continue on to [selecting which repositories to import](#selecting-which-repositories-to-import).
### Using a GitHub token
@@ -92,12 +92,12 @@ integration enabled, that should be the preferred method to import your reposito
If you are not using the GitHub integration, you can still perform an authorization with GitHub to grant GitLab access your repositories:
1. Go to https://github.com/settings/tokens/new
-2. Enter a token description.
-3. Select the repo scope.
-4. Click **Generate token**.
-5. Copy the token hash.
-6. Go back to GitLab and provide the token to the GitHub importer.
-7. Hit the **List Your GitHub Repositories** button and wait while GitLab reads your repositories' information.
+1. Enter a token description.
+1. Select the repo scope.
+1. Click **Generate token**.
+1. Copy the token hash.
+1. Go back to GitLab and provide the token to the GitHub importer.
+1. Hit the **List Your GitHub Repositories** button and wait while GitLab reads your repositories' information.
Once done, you'll be taken to the importer page to select the repositories to import.
### Selecting which repositories to import
@@ -107,10 +107,10 @@ your GitHub repositories are listed.
1. By default, the proposed repository namespaces match the names as they exist in GitHub, but based on your permissions,
you can choose to edit these names before you proceed to import any of them.
-2. Select the **Import** button next to any number of repositories, or select **Import all repositories**.
-3. The **Status** column shows the import status of each repository. You can choose to leave the page open and it will
+1. Select the **Import** button next to any number of repositories, or select **Import all repositories**.
+1. The **Status** column shows the import status of each repository. You can choose to leave the page open and it will
update in realtime or you can return to it later.
-4. Once a repository has been imported, click its GitLab path to open its GitLab URL.
+1. Once a repository has been imported, click its GitLab path to open its GitLab URL.
## Mirroring and pipeline status sharing
diff --git a/doc/user/project/import/index.md b/doc/user/project/import/index.md
index 4ea35a30bbf..2f5efbe84d9 100644
--- a/doc/user/project/import/index.md
+++ b/doc/user/project/import/index.md
@@ -1,6 +1,7 @@
# Migrating projects to a GitLab instance
-1. [From Bitbucket.org](bitbucket.md)
+1. [From Bitbucket Cloud (aka bitbucket.org)](bitbucket.md)
+1. [From Bitbucket Server (aka Stash)](bitbucket_server.md)
1. [From ClearCase](clearcase.md)
1. [From CVS](cvs.md)
1. [From FogBugz](fogbugz.md)
diff --git a/doc/user/project/import/manifest.md b/doc/user/project/import/manifest.md
index 24bf6541a9d..baf410d9c9e 100644
--- a/doc/user/project/import/manifest.md
+++ b/doc/user/project/import/manifest.md
@@ -29,7 +29,7 @@ Below is a valid example of a manifest file:
```xml
<manifest>
- <remote review="https://android-review.googlesource.com/" />
+ <remote review="https://android.googlesource.com/" />
<project path="build/make" name="platform/build" />
<project path="build/blueprint" name="platform/build/blueprint" />
@@ -38,10 +38,10 @@ Below is a valid example of a manifest file:
As a result, the following projects will be created:
-| GitLab | Import URL |
-|---|---|
-| https://gitlab.com/YOUR_GROUP/build/make | https://android-review.googlesource.com/platform/build |
-| https://gitlab.com/YOUR_GROUP/build/blueprint | https://android-review.googlesource.com/platform/build/blueprint |
+| GitLab | Import URL |
+|:------------------------------------------------|:------------------------------------------------------------|
+| `https://gitlab.com/YOUR_GROUP/build/make` | <https://android.googlesource.com/platform/build> |
+| `https://gitlab.com/YOUR_GROUP/build/blueprint` | <https://android.googlesource.com/platform/build/blueprint> |
## Importing the repositories
diff --git a/doc/user/project/integrations/bugzilla.md b/doc/user/project/integrations/bugzilla.md
index 671804035cc..040e80d529d 100644
--- a/doc/user/project/integrations/bugzilla.md
+++ b/doc/user/project/integrations/bugzilla.md
@@ -16,8 +16,9 @@ Once you have configured and enabled Bugzilla you'll see the Bugzilla link on th
## Referencing issues in Bugzilla
Issues in Bugzilla can be referenced in two alternative ways:
-1. `#<ID>` where `<ID>` is a number (example `#143`).
-2. `<PROJECT>-<ID>` where `<PROJECT>` starts with a capital letter which is
+
+- `#<ID>` where `<ID>` is a number (example `#143`).
+- `<PROJECT>-<ID>` where `<PROJECT>` starts with a capital letter which is
then followed by capital letters, numbers or underscores, and `<ID>` is
a number (example `API_32-143`).
diff --git a/doc/user/project/integrations/discord_notifications.md b/doc/user/project/integrations/discord_notifications.md
new file mode 100644
index 00000000000..e157f5cc106
--- /dev/null
+++ b/doc/user/project/integrations/discord_notifications.md
@@ -0,0 +1,29 @@
+# Discord Notifications service
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22684) in GitLab 11.5.
+
+The Discord Notifications service sends event notifications from GitLab to the channel for which the webhook was created.
+
+To send GitLab event notifications to a Discord channel, create a webhook in Discord and configure it in GitLab.
+
+## Create webhook
+
+1. Open the Discord channel you want to receive GitLab event notifications.
+1. From the channel menu, select **Edit channel**.
+1. Click on **Webhooks** menu item.
+1. Click the **Create Webhook** button and fill in the name of the bot that will post the messages. Optionally, edit the avatar.
+1. Note the URL from the **WEBHOOK URL** field.
+1. Click the **Save** button.
+
+## Configure created webhook in GitLab
+
+With the webhook URL created in the Discord channel, you can set up the Discord Notifications service in GitLab.
+
+1. Navigate to the [Integrations page](project_services.md#accessing-the-project-services) in your project's settings. That is, **Project > Settings > Integrations**.
+1. Select the **Discord Notifications** project service to configure it.
+1. Check the **Active** checkbox to turn on the service.
+1. Check the checkboxes corresponding to the GitLab events for which you want to send notifications to Discord.
+1. Paste the webhook URL that you copied from the create Discord webhook step.
+1. Configure the remaining options and click the **Save changes** button.
+
+The Discord channel you created the webhook for will now receive notification of the GitLab events that were configured.
diff --git a/doc/user/project/integrations/jira_cloud_configuration.md b/doc/user/project/integrations/jira_cloud_configuration.md
index 2e6e8278e64..cae66526175 100644
--- a/doc/user/project/integrations/jira_cloud_configuration.md
+++ b/doc/user/project/integrations/jira_cloud_configuration.md
@@ -4,16 +4,15 @@ An API token is needed when integrating with JIRA Cloud, follow the steps
below to create one:
1. Log in to https://id.atlassian.com with your email.
-2. **Click API tokens**, then **Create API token**.
+1. **Click API tokens**, then **Create API token**.
![JIRA API token](img/jira_api_token_menu.png)
![JIRA API token](img/jira_api_token.png)
-3. Make sure to write down your new API token as you will need it in the next [steps](jira.md#configuring-gitlab).
+1. Make sure to write down your new API token as you will need it in the next [steps](jira.md#configuring-gitlab).
NOTE: **Note**
It is important that the user associated with this email has 'write' access to projects in JIRA.
The JIRA configuration is complete. You are going to need this new created token and the email you used to log in when [configuring GitLab in the next section](jira.md#configuring-gitlab).
-
diff --git a/doc/user/project/integrations/jira_server_configuration.md b/doc/user/project/integrations/jira_server_configuration.md
index 7d84ad0b07c..20036183187 100644
--- a/doc/user/project/integrations/jira_server_configuration.md
+++ b/doc/user/project/integrations/jira_server_configuration.md
@@ -17,17 +17,17 @@ We have split this stage in steps so it is easier to follow.
![Jira user management link](img/jira_user_management_link.png)
-2. The next step is to create a new user (e.g., `gitlab`) who has write access
+1. The next step is to create a new user (e.g., `gitlab`) who has write access
to projects in Jira. Enter the user's name and a _valid_ e-mail address
since Jira sends a verification e-mail to set up the password.
_**Note:** Jira creates the username automatically by using the e-mail
- prefix. You can change it later, if needed. Our integration does not support SSO (such as SAML). You will need to create
- an HTTP basic authentication password. You can do this by visiting the user
+ prefix. You can change it later, if needed. Our integration does not support SSO (such as SAML). You will need to create
+ an HTTP basic authentication password. You can do this by visiting the user
profile, looking up the username, and setting a password._
![Jira create new user](img/jira_create_new_user.png)
-3. Create a `gitlab-developers` group which will have write access
+1. Create a `gitlab-developers` group which will have write access
to projects in Jira. Go to the **Groups** tab and select **Create group**.
![Jira create new user](img/jira_create_new_group.png)
@@ -36,13 +36,13 @@ We have split this stage in steps so it is easier to follow.
![Jira create new group](img/jira_create_new_group_name.png)
-4. To give the newly-created group 'write' access, go to
+1. To give the newly-created group 'write' access, go to
**Application access > View configuration** and add the `gitlab-developers`
group to Jira Core.
![Jira group access](img/jira_group_access.png)
-5. Add the `gitlab` user to the `gitlab-developers` group by going to
+1. Add the `gitlab` user to the `gitlab-developers` group by going to
**Users > GitLab user > Add group** and selecting the `gitlab-developers`
group from the dropdown menu. Notice that the group says _Access_, which is
intended as part of this process.
diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md
index 89de1fe4dcb..ed4367c1135 100644
--- a/doc/user/project/integrations/mattermost.md
+++ b/doc/user/project/integrations/mattermost.md
@@ -4,9 +4,9 @@
To enable Mattermost integration you must create an incoming webhook integration:
-1. Sign in to your Mattermost instance
-1. Visit incoming webhooks, that will be something like: https://mattermost.example/your_team_name/integrations/incoming_webhooks/add
-1. Choose a display name, description and channel, those can be overridden on GitLab
+1. Sign in to your Mattermost instance.
+1. Visit incoming webhooks, that will be something like: `https://mattermost.example.com/your_team_name/integrations/incoming_webhooks/add`.
+1. Choose a display name, description and channel, those can be overridden on GitLab.
1. Save it, copy the **Webhook URL**, we'll need this later for GitLab.
There might be some cases that Incoming Webhooks are blocked by admin, ask your mattermost admin to enable
diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md
index efb0381d7aa..be45ce46dfd 100644
--- a/doc/user/project/integrations/project_services.md
+++ b/doc/user/project/integrations/project_services.md
@@ -30,6 +30,7 @@ Click on the service links to see further configuration instructions and details
| [Bugzilla](bugzilla.md) | Bugzilla issue tracker |
| Campfire | Simple web-based real-time group chat |
| Custom Issue Tracker | Custom issue tracker |
+| [Discord Notifications](discord_notifications.md) | Receive event notifications in Discord |
| Drone CI | Continuous Integration platform built on Docker, written in Go |
| [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 |
diff --git a/doc/user/project/integrations/redmine.md b/doc/user/project/integrations/redmine.md
index de2cf6d4647..76a2617125e 100644
--- a/doc/user/project/integrations/redmine.md
+++ b/doc/user/project/integrations/redmine.md
@@ -18,15 +18,16 @@ in the table below.
![Redmine configuration](img/redmine_configuration.png)
-2. To disable the internal issue tracking system in a project, navigate to the General page, expand [Permissions](../settings/index.md#sharing-and-permissions), and slide the Issues switch invalid.
+1. To disable the internal issue tracking system in a project, navigate to the General page, expand [Permissions](../settings/index.md#sharing-and-permissions), and slide the Issues switch invalid.
![Issue configuration](img/issue_configuration.png)
## Referencing issues in Redmine
Issues in Redmine can be referenced in two alternative ways:
-1. `#<ID>` where `<ID>` is a number (example `#143`)
-2. `<PROJECT>-<ID>` where `<PROJECT>` starts with a capital letter which is
+
+- `#<ID>` where `<ID>` is a number (example `#143`).
+- `<PROJECT>-<ID>` where `<PROJECT>` starts with a capital letter which is
then followed by capital letters, numbers or underscores, and `<ID>` is
a number (example `API_32-143`).
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index 7c63967c829..4d1d95da6f0 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -338,10 +338,10 @@ payload will also include information about the target of the comment. For examp
a comment on an issue will include the specific issue information under the `issue` key.
Valid target types:
-1. `commit`
-2. `merge_request`
-3. `issue`
-4. `snippet`
+- `commit`
+- `merge_request`
+- `issue`
+- `snippet`
#### Comment on commit
diff --git a/doc/user/project/issues/automatic_issue_closing.md b/doc/user/project/issues/automatic_issue_closing.md
index b6570c777ae..afb7d9ada5f 100644
--- a/doc/user/project/issues/automatic_issue_closing.md
+++ b/doc/user/project/issues/automatic_issue_closing.md
@@ -27,10 +27,11 @@ used:
Note that `%{issue_ref}` is a complex regular expression defined inside GitLab's
source code that can match references to:
-1. a local issue (`#123`),
-2. a cross-project issue (`group/project#123`)
-3. a link to an issue
-(`https://gitlab.example.com/group/project/issues/123`).
+
+- A local issue (`#123`).
+- A cross-project issue (`group/project#123`).
+- A link to an issue
+ (`https://gitlab.example.com/group/project/issues/123`).
---
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index f9ebf277125..6de2ab07fc4 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -166,6 +166,23 @@ administrator to do so.
![Create new merge requests by email](img/create_from_email.png)
+### Adding patches when creating a merge request via e-mail
+
+> **Note**: This feature was [implemented in GitLab 11.5](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22723)
+
+You can add commits to the merge request being created by adding
+patches as attachments to the email, all attachments with a filename
+ending in `.patch` will be considered patches. The patches will be processed
+ordered by name.
+
+The combined size of the patches can be 2MB.
+
+If the source branch from the subject does not exist, it will be
+created from the repository's HEAD or the specified target branch to
+apply the patches. The target branch can be specified using the
+[`/target_branch` quick action](../quick_actions.md). If the source
+branch already exists, the patches will be applied on top of it.
+
## Find the merge request that introduced a change
> **Note**: this feature was [implemented in GitLab 10.5](https://gitlab.com/gitlab-org/gitlab-ce/issues/2383).
@@ -236,6 +253,35 @@ all your changes will be available to preview by anyone with the Review Apps lin
Find out about [bulk editing merge requests](../../project/bulk_editing.md).
+## Troubleshooting
+
+Sometimes things don't go as expected in a merge request, here are some
+troubleshooting steps.
+
+### Merge request cannot retrieve the pipeline status
+
+This can occur for one of two reasons:
+
+* Sidekiq doesn't pick up the changes fast enough
+* Because of the bug described in [#41545](https://gitlab.com/gitlab-org/gitlab-ce/issues/41545)
+
+#### Sidekiq
+
+Sidekiq didn't process the CI state change fast enough. Please wait a few
+seconds and the status will update automatically.
+
+#### Bug
+
+Merge Request pipeline statuses can't be retrieved when the following occurs:
+
+1. A Merge Requst is created
+1. The Merge Request is closed
+1. Changes are made in the project
+1. The Merge Request is reopened
+
+To enable the pipeline status to be properly retrieved, close and reopen the
+Merge Request again.
+
## Tips
Here are some tips that will help you be more efficient with merge requests in
diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md
index 23d5b34504c..9a53036b4d1 100644
--- a/doc/user/project/new_ci_build_permissions_model.md
+++ b/doc/user/project/new_ci_build_permissions_model.md
@@ -60,7 +60,7 @@ Let's consider the following scenario:
hosted in private repositories and you have multiple CI jobs that make use
of these repositories.
-2. You invite a new [external user][ext]. CI jobs created by that user do not
+1. You invite a new [external user][ext]. CI jobs created by that user do not
have access to internal repositories, because the user also doesn't have the
access from within GitLab. You as an employee have to grant explicit access
for this user. This allows us to prevent from accidental data leakage.
diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md
index beff4b89424..1710bba2fd0 100644
--- a/doc/user/project/repository/index.md
+++ b/doc/user/project/repository/index.md
@@ -53,6 +53,46 @@ To get started with the command line, please read through the
Use GitLab's [file finder](../../../workflow/file_finder.md) to search for files in a repository.
+### Supported markup languages and extensions
+
+GitLab supports a number of markup languages (sometimes called [lightweight
+markup languages](https://en.wikipedia.org/wiki/Lightweight_markup_language))
+that you can use for the content of your files in a repository. They are mostly
+used for documentation purposes.
+
+Just pick the right extension for your files and GitLab will render them
+according to the markup language.
+
+| Markup language | Extensions |
+| --------------- | ---------- |
+| Plain text | `txt` |
+| [Markdown](../../markdown.md) | `mdown`, `mkd`, `mkdn`, `md`, `markdown` |
+| [reStructuredText](http://docutils.sourceforge.net/rst.html) | `rst` |
+| [Asciidoc](https://asciidoctor.org/docs/what-is-asciidoc/) | `adoc`, `ad`, `asciidoc` |
+| [Textile](https://txstyle.org/) | `textile` |
+| [rdoc](http://rdoc.sourceforge.net/doc/index.html) | `rdoc` |
+| [Orgmode](https://orgmode.org/) | `org` |
+| [creole](http://www.wikicreole.org/) | `creole` |
+| [Mediawiki](https://www.mediawiki.org/wiki/MediaWiki) | `wiki`, `mediawiki` |
+
+### Repository README and index files
+
+When a `README` or `index` file is present in a repository, its contents will be
+automatically pre-rendered by GitLab without opening it.
+
+They can either be plain text or have an extension of a
+[supported markup language](#supported-markup-languages-and-extensions):
+
+Some things to note about precedence:
+
+1. When both a `README` and an `index` file are present, the `README` will always
+ take precedence.
+1. When more than one file is present with different extensions, they are
+ ordered alphabetically, with the exception of a file without an extension
+ which will always be last in precedence. For example, `README.adoc` will take
+ precedence over `README.md`, and `README.rst` will take precedence over
+ `README`.
+
### Jupyter Notebook files
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/2508) in GitLab 9.1
@@ -165,7 +205,7 @@ minutes.
![Repository Languages bar](img/repository_languages.png)
-Not all files are detected, among others; documentation,
+Not all files are detected, among others; documentation,
vendored code, and most markup languages are excluded. This behaviour can be
adjusted by overriding the default. For example, to enable `.proto` files to be
detected, add the following to `.gitattributes` in the root of your repository.
diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md
index 731c9209224..c590ac4b0ba 100644
--- a/doc/workflow/notifications.md
+++ b/doc/workflow/notifications.md
@@ -10,31 +10,32 @@ You can find notification settings under the user profile.
Notification settings are divided into three groups:
-* Global Settings
-* Group Settings
-* Project Settings
+- Global settings
+- Group settings
+- Project settings
Each of these settings have levels of notification:
-* Disabled - turns off notifications
-* Participating - receive notifications from related resources
-* Watch - receive notifications from projects or groups user is a member of
-* Global - notifications as set at the global settings
-* Custom - user will receive notifications when mentioned, is participant and custom selected events.
+- Watch: Receive notifications for any activity.
+- On Mention: Receive notifications when `@mentioned` in comments.
+- Participate: Receive notifications for threads you have participated in.
+- Disabled: Turns off notifications.
+- Custom: Receive notifications for custom selected events.
+- Global: For groups and projects, notifications as per global settings.
-#### Global Settings
+### Global Settings
-Global Settings are at the bottom of the hierarchy.
+Global settings are at the bottom of the hierarchy.
Any setting set here will be overridden by a setting at the group or a project level.
Group or Project settings can use `global` notification setting which will then use
anything that is set at Global Settings.
-#### Group Settings
+### Group Settings
![notification settings](img/notification_group_settings.png)
-Group Settings are taking precedence over Global Settings but are on a level below Project or Subgroup Settings:
+Group settings are taking precedence over Global Settings but are on a level below Project or Subgroup settings:
```
Group < Subgroup < Project
@@ -46,11 +47,11 @@ Organization like this is suitable for users that belong to different groups but
same need for being notified for every group they are member of.
These settings can be configured on group page under the name of the group. It will be the dropdown with the bell icon. They can also be configured on the user profile notifications dropdown.
-#### Project Settings
+### Project Settings
![notification settings](img/notification_project_settings.png)
-Project Settings are at the top level and any setting placed at this level will take precedence of any
+Project settings are at the top level and any setting placed at this level will take precedence of any
other setting.
This is suitable for users that have different needs for notifications per project basis.
These settings can be configured on project page under the name of the project. It will be the dropdown with the bell icon. They can also be configured on the user profile notifications dropdown.
@@ -73,11 +74,12 @@ Below is the table of events users can be notified of:
### Issue / 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
+ - anyone mentioned by `@username` in any of the comments on the issue/merge request
...with notification level "Participating" or higher
- Watchers: users with notification level "Watch"
- Subscribers: anyone who manually subscribed to the issue/merge request
@@ -90,12 +92,16 @@ In most of the below cases, the notification will be sent to:
| Reassign issue | The above, plus the old assignee |
| Reopen issue | |
| Due issue | Participants and Custom notification level with this event selected |
+| Change milestone issue | Subscribers, participants mentioned, and Custom notification level with this event selected |
+| Remove milestone issue | Subscribers, participants mentioned, and Custom notification level with this event selected |
| New merge request | |
| Push to merge request | Participants and Custom notification level with this event selected |
| Reassign merge request | The above, plus the old assignee |
| Close merge request | |
| Reopen merge request | |
| Merge merge request | |
+| Change milestone merge request | Subscribers, participants mentioned, and Custom notification level with this event selected |
+| Remove milestone merge request | Subscribers, participants mentioned, and Custom notification level with this event selected |
| New comment | The above, plus anyone mentioned by `@username` in the comment, with notification level "Mention" or higher |
| Failed pipeline | The author of the pipeline |
| Successful pipeline | The author of the pipeline, if they have the custom notification setting for successful pipelines set |
@@ -129,16 +135,19 @@ Notification emails include headers that provide extra content about the notific
| X-GitLab-NotificationReason | The reason for being notified. "mentioned", "assigned", etc |
#### X-GitLab-NotificationReason
+
This header holds the reason for the notification to have been sent out,
where reason can be `mentioned`, `assigned`, `own_activity`, etc.
Only one reason is sent out according to its priority:
+
- `own_activity`
- `assigned`
- `mentioned`
-The reason in this header will also be shown in the footer of the notification email. For example an email with the
+The reason in this header will also be shown in the footer of the notification email. For example an email with the
reason `assigned` will have this sentence in the footer:
`"You are receiving this email because you have been assigned an item on {configured GitLab hostname}"`
-**Note: Only reasons listed above have been implemented so far**
-Further implementation is [being discussed here](https://gitlab.com/gitlab-org/gitlab-ce/issues/42062)
+NOTE: **Note:**
+Only reasons listed above have been implemented so far.
+Further implementation is [being discussed](https://gitlab.com/gitlab-org/gitlab-ce/issues/42062).
diff --git a/doc/workflow/shortcuts.md b/doc/workflow/shortcuts.md
index b2f1cbec204..7863dd8c242 100644
--- a/doc/workflow/shortcuts.md
+++ b/doc/workflow/shortcuts.md
@@ -93,4 +93,4 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?'
| Keyboard Shortcut | Description |
| ----------------- | ----------- |
-| <kbd>⌘</kbd> + <kbd>p</kbd> | Go to file |
+| <kbd>Cmd</kbd>/<kbd>Ctrl</kbd> + <kbd>p</kbd> | Go to file |
diff --git a/lib/api/api.rb b/lib/api/api.rb
index c49c52213bf..8e259961828 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -143,6 +143,7 @@ module API
mount ::API::Settings
mount ::API::SidekiqMetrics
mount ::API::Snippets
+ mount ::API::Submodules
mount ::API::Subscriptions
mount ::API::SystemHooks
mount ::API::Tags
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index e59abd3e3d0..3b8f3fedccf 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -194,11 +194,47 @@ module API
branch_name: params[:branch]
}
- result = ::Commits::CherryPickService.new(user_project, current_user, commit_params).execute
+ result = ::Commits::CherryPickService
+ .new(user_project, current_user, commit_params)
+ .execute
if result[:status] == :success
- branch = find_branch!(params[:branch])
- present user_project.repository.commit(branch.dereferenced_target), with: Entities::Commit
+ present user_project.repository.commit(result[:result]),
+ with: Entities::Commit
+ else
+ render_api_error!(result[:message], 400)
+ end
+ end
+
+ desc 'Revert a commit in a branch' do
+ detail 'This feature was introduced in GitLab 11.6'
+ success Entities::Commit
+ end
+ params do
+ requires :sha, type: String, desc: 'Commit SHA to revert'
+ requires :branch, type: String, desc: 'Target branch name', allow_blank: false
+ end
+ post ':id/repository/commits/:sha/revert', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
+ authorize_push_to_branch!(params[:branch])
+
+ commit = user_project.commit(params[:sha])
+ not_found!('Commit') unless commit
+
+ find_branch!(params[:branch])
+
+ commit_params = {
+ commit: commit,
+ start_branch: params[:branch],
+ branch_name: params[:branch]
+ }
+
+ result = ::Commits::RevertService
+ .new(user_project, current_user, commit_params)
+ .execute
+
+ if result[:status] == :success
+ present user_project.repository.commit(result[:result]),
+ with: Entities::Commit
else
render_api_error!(result[:message], 400)
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 9f7be27b047..61d57c643f0 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -607,6 +607,22 @@ module API
end
class MergeRequestBasic < ProjectEntity
+ expose :merged_by, using: Entities::UserBasic do |merge_request, _options|
+ merge_request.metrics&.merged_by
+ end
+
+ expose :merged_at do |merge_request, _options|
+ merge_request.metrics&.merged_at
+ end
+
+ expose :closed_by, using: Entities::UserBasic do |merge_request, _options|
+ merge_request.metrics&.latest_closed_by
+ end
+
+ expose :closed_at do |merge_request, _options|
+ merge_request.metrics&.latest_closed_at
+ end
+
expose :title_html, if: -> (_, options) { options[:render_html] } do |entity|
MarkupHelper.markdown_field(entity, :title)
end
@@ -676,22 +692,6 @@ module API
merge_request.merge_request_diff.real_size
end
- expose :merged_by, using: Entities::UserBasic do |merge_request, _options|
- merge_request.metrics&.merged_by
- end
-
- expose :merged_at do |merge_request, _options|
- merge_request.metrics&.merged_at
- end
-
- expose :closed_by, using: Entities::UserBasic do |merge_request, _options|
- merge_request.metrics&.latest_closed_by
- end
-
- expose :closed_at do |merge_request, _options|
- merge_request.metrics&.latest_closed_at
- end
-
expose :latest_build_started_at, if: -> (_, options) { build_available?(options) } do |merge_request, _options|
merge_request.metrics&.latest_build_started_at
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 405fc30a2ed..491b5085bb8 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -8,6 +8,15 @@ module API
helpers ::Gitlab::IssuableMetadata
+ # EE::API::Issues would override the following helpers
+ helpers do
+ params :issues_params_ee do
+ end
+
+ params :issue_params_ee do
+ end
+ end
+
helpers do
# rubocop: disable CodeReuse/ActiveRecord
def find_issues(args = {})
@@ -19,7 +28,7 @@ module API
args[:scope] = args[:scope].underscore if args[:scope]
issues = IssuesFinder.new(current_user, args).execute
- .preload(:assignees, :labels, :notes, :timelogs, :project, :author)
+ .preload(:assignees, :labels, :notes, :timelogs, :project, :author, :closed_by)
issues.reorder(args[:order_by] => args[:sort])
end
@@ -46,9 +55,11 @@ module API
desc: 'Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`'
optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji'
use :pagination
+
+ use :issues_params_ee
end
- params :issue_params_ce do
+ params :issue_params do
optional :description, type: String, desc: 'The description of an issue'
optional :assignee_ids, type: Array[Integer], desc: 'The array of user IDs to assign issue'
optional :assignee_id, type: Integer, desc: '[Deprecated] The ID of a user to assign issue'
@@ -57,10 +68,8 @@ module API
optional :due_date, type: String, desc: 'Date string in the format YEAR-MONTH-DAY'
optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential'
optional :discussion_locked, type: Boolean, desc: " Boolean parameter indicating if the issue's discussion is locked"
- end
- params :issue_params do
- use :issue_params_ce
+ use :issue_params_ee
end
end
@@ -285,6 +294,30 @@ module API
end
# rubocop: enable CodeReuse/ActiveRecord
+ desc 'List merge requests that are related to the issue' do
+ success Entities::MergeRequestBasic
+ end
+ params do
+ requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
+ end
+ get ':id/issues/:issue_iid/related_merge_requests' do
+ issue = find_project_issue(params[:issue_iid])
+
+ merge_request_iids = ::Issues::ReferencedMergeRequestsService.new(user_project, current_user)
+ .execute(issue)
+ .flatten
+ .map(&:iid)
+
+ merge_requests =
+ if merge_request_iids.present?
+ MergeRequestsFinder.new(current_user, project_id: user_project.id, iids: merge_request_iids).execute
+ else
+ MergeRequest.none
+ end
+
+ present paginate(merge_requests), with: Entities::MergeRequestBasic, current_user: current_user, project: user_project
+ end
+
desc 'List merge requests closing issue' do
success Entities::MergeRequestBasic
end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index a617efaaa4c..16f07f16387 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -45,7 +45,7 @@ module API
return merge_requests if args[:view] == 'simple'
merge_requests
- .preload(:notes, :author, :assignee, :milestone, :latest_merge_request_diff, :labels, :timelogs)
+ .preload(:notes, :author, :assignee, :milestone, :latest_merge_request_diff, :labels, :timelogs, metrics: [:latest_closed_by, :merged_by])
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/lib/api/services.rb b/lib/api/services.rb
index 0ae05ce08f1..1cb3b8a7277 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -298,6 +298,14 @@ module API
desc: 'Title'
}
],
+ 'discord' => [
+ {
+ required: true,
+ name: :webhook,
+ type: String,
+ desc: 'Discord webhook. e.g. https://discordapp.com/api/webhooks/…'
+ }
+ ],
'drone-ci' => [
{
required: true,
@@ -677,6 +685,7 @@ module API
BuildkiteService,
CampfireService,
CustomIssueTrackerService,
+ DiscordService,
DroneCiService,
EmailsOnPushService,
ExternalWikiService,
diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb
index f1786c15f4f..1ae144ca9c1 100644
--- a/lib/api/snippets.rb
+++ b/lib/api/snippets.rb
@@ -14,7 +14,7 @@ module API
end
def public_snippets
- SnippetsFinder.new(current_user, visibility: Snippet::PUBLIC).execute
+ SnippetsFinder.new(current_user, scope: :are_public).execute
end
end
diff --git a/lib/api/submodules.rb b/lib/api/submodules.rb
new file mode 100644
index 00000000000..72d7d994102
--- /dev/null
+++ b/lib/api/submodules.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module API
+ class Submodules < Grape::API
+ before { authenticate! }
+
+ helpers do
+ def commit_params(attrs)
+ {
+ submodule: attrs[:submodule],
+ commit_sha: attrs[:commit_sha],
+ branch_name: attrs[:branch],
+ commit_message: attrs[:commit_message]
+ }
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The project ID'
+ end
+ resource :projects, requirements: Files::FILE_ENDPOINT_REQUIREMENTS do
+ desc 'Update existing submodule reference in repository' do
+ success Entities::Commit
+ end
+ params do
+ requires :submodule, type: String, desc: 'Url encoded full path to submodule.'
+ requires :commit_sha, type: String, desc: 'Commit sha to update the submodule to.'
+ requires :branch, type: String, desc: 'Name of the branch to commit into.'
+ optional :commit_message, type: String, desc: 'Commit message. If no message is provided a default one will be set.'
+ end
+ put ":id/repository/submodules/:submodule", requirements: Files::FILE_ENDPOINT_REQUIREMENTS do
+ authorize! :push_code, user_project
+
+ submodule_params = declared_params(include_missing: false)
+
+ result = ::Submodules::UpdateService.new(user_project, current_user, commit_params(submodule_params)).execute
+
+ if result[:status] == :success
+ commit_detail = user_project.repository.commit(result[:result])
+ present commit_detail, with: Entities::CommitDetail
+ else
+ render_api_error!(result[:message], result[:http_status] || 400)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 47382b09207..2a56506f3a5 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -512,11 +512,9 @@ module API
PersonalAccessTokensFinder.new({ user: user, impersonation: true }.merge(options))
end
- # rubocop: disable CodeReuse/ActiveRecord
def find_impersonation_token
- finder.find_by(id: declared_params[:impersonation_token_id]) || not_found!('Impersonation Token')
+ finder.find_by_id(declared_params[:impersonation_token_id]) || not_found!('Impersonation Token')
end
- # rubocop: enable CodeReuse/ActiveRecord
end
before { authenticated_as_admin! }
diff --git a/lib/banzai/filter/absolute_link_filter.rb b/lib/banzai/filter/absolute_link_filter.rb
index 04ec568eee3..a9bdb004c4b 100644
--- a/lib/banzai/filter/absolute_link_filter.rb
+++ b/lib/banzai/filter/absolute_link_filter.rb
@@ -29,6 +29,7 @@ module Banzai
end
def absolute_link_attr(uri)
+ # Here we really want to expand relative path to absolute path
URI.join(Gitlab.config.gitlab.url, uri).to_s
end
end
diff --git a/lib/banzai/filter/issuable_state_filter.rb b/lib/banzai/filter/issuable_state_filter.rb
index d7fe012883d..8e2358694d4 100644
--- a/lib/banzai/filter/issuable_state_filter.rb
+++ b/lib/banzai/filter/issuable_state_filter.rb
@@ -18,7 +18,7 @@ module Banzai
issuables = extractor.extract([doc])
issuables.each do |node, issuable|
- next if !can_read_cross_project? && issuable.project != project
+ next if !can_read_cross_project? && cross_reference?(issuable)
if VISIBLE_STATES.include?(issuable.state) && issuable_reference?(node.inner_html, issuable)
node.content += " (#{issuable.state})"
@@ -31,7 +31,14 @@ module Banzai
private
def issuable_reference?(text, issuable)
- text == issuable.reference_link_text(project || group)
+ CGI.unescapeHTML(text) == issuable.reference_link_text(project || group)
+ end
+
+ def cross_reference?(issuable)
+ return true if issuable.project != project
+ return true if issuable.respond_to?(:group) && issuable.group != group
+
+ false
end
def can_read_cross_project?
diff --git a/lib/banzai/issuable_extractor.rb b/lib/banzai/issuable_extractor.rb
index 0a05d46db4c..341dbb74fe0 100644
--- a/lib/banzai/issuable_extractor.rb
+++ b/lib/banzai/issuable_extractor.rb
@@ -9,13 +9,11 @@ module Banzai
# so we can avoid N+1 queries problem
class IssuableExtractor
- QUERY = %q(
- descendant-or-self::a[contains(concat(" ", @class, " "), " gfm ")]
- [@data-reference-type="issue" or @data-reference-type="merge_request"]
- ).freeze
-
attr_reader :context
+ ISSUE_REFERENCE_TYPE = '@data-reference-type="issue"'.freeze
+ MERGE_REQUEST_REFERENCE_TYPE = '@data-reference-type="merge_request"'.freeze
+
# context - An instance of Banzai::RenderContext.
def initialize(context)
@context = context
@@ -24,21 +22,38 @@ module Banzai
# Returns Hash in the form { node => issuable_instance }
def extract(documents)
nodes = documents.flat_map do |document|
- document.xpath(QUERY)
+ document.xpath(query)
end
- issue_parser = Banzai::ReferenceParser::IssueParser.new(context)
+ # The project or group for the issuable might be pending for deletion!
+ # Filter them out because we don't care about them.
+ issuables_for_nodes(nodes).select { |node, issuable| issuable.project || issuable.group }
+ end
+
+ private
- merge_request_parser =
+ def issuables_for_nodes(nodes)
+ parsers.each_with_object({}) do |parser, result|
+ result.merge!(parser.records_for_nodes(nodes))
+ end
+ end
+
+ def parsers
+ [
+ Banzai::ReferenceParser::IssueParser.new(context),
Banzai::ReferenceParser::MergeRequestParser.new(context)
+ ]
+ end
- issuables_for_nodes = issue_parser.records_for_nodes(nodes).merge(
- merge_request_parser.records_for_nodes(nodes)
+ def query
+ %Q(
+ descendant-or-self::a[contains(concat(" ", @class, " "), " gfm ")]
+ [#{reference_types.join(' or ')}]
)
+ end
- # The project for the issue/MR might be pending for deletion!
- # Filter them out because we don't care about them.
- issuables_for_nodes.select { |node, issuable| issuable.project }
+ def reference_types
+ [ISSUE_REFERENCE_TYPE, MERGE_REQUEST_REFERENCE_TYPE]
end
end
end
diff --git a/lib/bitbucket_server/client.rb b/lib/bitbucket_server/client.rb
index 15e59f93141..83e8808db07 100644
--- a/lib/bitbucket_server/client.rb
+++ b/lib/bitbucket_server/client.rb
@@ -35,9 +35,9 @@ module BitbucketServer
BitbucketServer::Representation::Repo.new(parsed_response)
end
- def repos
+ def repos(page_offset: 0, limit: nil)
path = "/repos"
- get_collection(path, :repo)
+ get_collection(path, :repo, page_offset: page_offset, limit: limit)
end
def create_branch(project_key, repo, branch_name, sha)
@@ -61,8 +61,8 @@ module BitbucketServer
private
- def get_collection(path, type)
- paginator = BitbucketServer::Paginator.new(connection, Addressable::URI.escape(path), type)
+ def get_collection(path, type, page_offset: 0, limit: nil)
+ paginator = BitbucketServer::Paginator.new(connection, Addressable::URI.escape(path), type, page_offset: page_offset, limit: limit)
BitbucketServer::Collection.new(paginator)
rescue *SERVER_ERRORS => e
raise ServerError, e
diff --git a/lib/bitbucket_server/collection.rb b/lib/bitbucket_server/collection.rb
index b50c5dde352..7e4b2277bbe 100644
--- a/lib/bitbucket_server/collection.rb
+++ b/lib/bitbucket_server/collection.rb
@@ -2,7 +2,13 @@
module BitbucketServer
class Collection < Enumerator
+ attr_reader :paginator
+
+ delegate :page_offset, :has_next_page?, to: :paginator
+
def initialize(paginator)
+ @paginator = paginator
+
super() do |yielder|
loop do
paginator.items.each { |item| yielder << item }
@@ -12,6 +18,24 @@ module BitbucketServer
lazy
end
+ def current_page
+ return 1 if page_offset <= 1
+
+ [1, page_offset].max
+ end
+
+ def prev_page
+ return nil unless current_page > 1
+
+ current_page - 1
+ end
+
+ def next_page
+ return nil unless has_next_page?
+
+ current_page + 1
+ end
+
def method_missing(method, *args)
return super unless self.respond_to?(method)
diff --git a/lib/bitbucket_server/connection.rb b/lib/bitbucket_server/connection.rb
index 45a437844bd..7efcdcf8619 100644
--- a/lib/bitbucket_server/connection.rb
+++ b/lib/bitbucket_server/connection.rb
@@ -88,35 +88,19 @@ module BitbucketServer
def build_url(path)
return path if path.starts_with?(root_url)
- url_join_paths(root_url, path)
+ Gitlab::Utils.append_path(root_url, path)
end
def root_url
- url_join_paths(base_uri, "/rest/api/#{api_version}")
+ Gitlab::Utils.append_path(base_uri, "rest/api/#{api_version}")
end
def delete_url(resource, path)
if resource == :branches
- url_join_paths(base_uri, "/rest/branch-utils/#{api_version}#{path}")
+ Gitlab::Utils.append_path(base_uri, "rest/branch-utils/#{api_version}#{path}")
else
build_url(path)
end
end
-
- # URI.join is stupid in that slashes are important:
- #
- # # URI.join('http://example.com/subpath', 'hello')
- # => http://example.com/hello
- #
- # We really want http://example.com/subpath/hello
- #
- def url_join_paths(*paths)
- paths.map { |path| strip_slashes(path) }.join(SEPARATOR)
- end
-
- def strip_slashes(path)
- path = path[1..-1] if path.starts_with?(SEPARATOR)
- path.chomp(SEPARATOR)
- end
end
end
diff --git a/lib/bitbucket_server/paginator.rb b/lib/bitbucket_server/paginator.rb
index c351fb2f11f..aa5f84f44b3 100644
--- a/lib/bitbucket_server/paginator.rb
+++ b/lib/bitbucket_server/paginator.rb
@@ -4,34 +4,49 @@ module BitbucketServer
class Paginator
PAGE_LENGTH = 25
- def initialize(connection, url, type)
+ attr_reader :page_offset
+
+ def initialize(connection, url, type, page_offset: 0, limit: nil)
@connection = connection
@type = type
@url = url
@page = nil
+ @page_offset = page_offset
+ @limit = limit || PAGE_LENGTH
+ @total = 0
end
def items
raise StopIteration unless has_next_page?
+ raise StopIteration if over_limit?
@page = fetch_next_page
+ @total += @page.items.count
@page.items
end
+ def has_next_page?
+ page.nil? || page.next?
+ end
+
private
- attr_reader :connection, :page, :url, :type
+ attr_reader :connection, :page, :url, :type, :limit
- def has_next_page?
- page.nil? || page.next?
+ def over_limit?
+ @limit.positive? && @total >= @limit
end
def next_offset
- page.nil? ? 0 : page.next
+ page.nil? ? starting_offset : page.next
+ end
+
+ def starting_offset
+ [0, page_offset - 1].max * limit
end
def fetch_next_page
- parsed_response = connection.get(@url, start: next_offset, limit: PAGE_LENGTH)
+ parsed_response = connection.get(@url, start: next_offset, limit: @limit)
Page.new(parsed_response, type)
end
end
diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb
index a340a276640..655278da711 100644
--- a/lib/extracts_path.rb
+++ b/lib/extracts_path.rb
@@ -153,7 +153,7 @@ module ExtractsPath
private
- # overriden in subclasses, do not remove
+ # overridden in subclasses, do not remove
def get_id
id = [params[:id] || params[:ref]]
id << "/" + params[:path] unless params[:path].blank?
diff --git a/lib/gitlab/auth/o_auth/auth_hash.rb b/lib/gitlab/auth/o_auth/auth_hash.rb
index 4a5f9d2839d..36fc8061d92 100644
--- a/lib/gitlab/auth/o_auth/auth_hash.rb
+++ b/lib/gitlab/auth/o_auth/auth_hash.rb
@@ -80,7 +80,7 @@ module Gitlab
end
# Get the first part of the email address (before @)
- # In addtion in removes illegal characters
+ # In addition in removes illegal characters
def generate_username(email)
email.match(/^[^@]*/)[0].mb_chars.normalize(:kd).gsub(/[^\x00-\x7F]/, '').to_s
end
diff --git a/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table.rb b/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table.rb
new file mode 100644
index 00000000000..35bfc381180
--- /dev/null
+++ b/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+#
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class PopulateClusterKubernetesNamespaceTable
+ include Gitlab::Database::MigrationHelpers
+
+ BATCH_SIZE = 1_000
+
+ module Migratable
+ class KubernetesNamespace < ActiveRecord::Base
+ self.table_name = 'clusters_kubernetes_namespaces'
+ end
+
+ class ClusterProject < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'cluster_projects'
+
+ belongs_to :project
+
+ def self.with_no_kubernetes_namespace
+ where.not(id: Migratable::KubernetesNamespace.select(:cluster_project_id))
+ end
+
+ def namespace
+ slug = "#{project.path}-#{project.id}".downcase
+ slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '')
+ end
+
+ def service_account
+ "#{namespace}-service-account"
+ end
+ end
+
+ class Project < ActiveRecord::Base
+ self.table_name = 'projects'
+ end
+ end
+
+ def perform
+ cluster_projects_with_no_kubernetes_namespace.each_batch(of: BATCH_SIZE) do |cluster_projects_batch, index|
+ sql_values = sql_values_for(cluster_projects_batch)
+
+ insert_into_cluster_kubernetes_namespace(sql_values)
+ end
+ end
+
+ private
+
+ def cluster_projects_with_no_kubernetes_namespace
+ Migratable::ClusterProject.with_no_kubernetes_namespace
+ end
+
+ def sql_values_for(cluster_projects)
+ cluster_projects.map do |cluster_project|
+ values_for_cluster_project(cluster_project)
+ end
+ end
+
+ def values_for_cluster_project(cluster_project)
+ {
+ cluster_project_id: cluster_project.id,
+ cluster_id: cluster_project.cluster_id,
+ project_id: cluster_project.project_id,
+ namespace: cluster_project.namespace,
+ service_account_name: cluster_project.service_account,
+ created_at: 'NOW()',
+ updated_at: 'NOW()'
+ }
+ end
+
+ def insert_into_cluster_kubernetes_namespace(rows)
+ Gitlab::Database.bulk_insert(Migratable::KubernetesNamespace.table_name,
+ rows,
+ disable_quote: [:created_at, :updated_at])
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/redact_links.rb b/lib/gitlab/background_migration/redact_links.rb
index f5d3bcdd517..92256e59a6c 100644
--- a/lib/gitlab/background_migration/redact_links.rb
+++ b/lib/gitlab/background_migration/redact_links.rb
@@ -1,25 +1,14 @@
# frozen_string_literal: true
# rubocop:disable Style/Documentation
+require_relative 'redact_links/redactable'
+
module Gitlab
module BackgroundMigration
class RedactLinks
- module Redactable
- extend ActiveSupport::Concern
-
- def redact_field!(field)
- self[field].gsub!(%r{/sent_notifications/\h{32}/unsubscribe}, '/sent_notifications/REDACTED/unsubscribe')
-
- if self.changed?
- self.update_columns(field => self[field],
- "#{field}_html" => nil)
- end
- end
- end
-
class Note < ActiveRecord::Base
include EachBatch
- include Redactable
+ include ::Gitlab::BackgroundMigration::RedactLinks::Redactable
self.table_name = 'notes'
self.inheritance_column = :_type_disabled
@@ -27,7 +16,7 @@ module Gitlab
class Issue < ActiveRecord::Base
include EachBatch
- include Redactable
+ include ::Gitlab::BackgroundMigration::RedactLinks::Redactable
self.table_name = 'issues'
self.inheritance_column = :_type_disabled
@@ -35,7 +24,7 @@ module Gitlab
class MergeRequest < ActiveRecord::Base
include EachBatch
- include Redactable
+ include ::Gitlab::BackgroundMigration::RedactLinks::Redactable
self.table_name = 'merge_requests'
self.inheritance_column = :_type_disabled
@@ -43,7 +32,7 @@ module Gitlab
class Snippet < ActiveRecord::Base
include EachBatch
- include Redactable
+ include ::Gitlab::BackgroundMigration::RedactLinks::Redactable
self.table_name = 'snippets'
self.inheritance_column = :_type_disabled
diff --git a/lib/gitlab/background_migration/redact_links/redactable.rb b/lib/gitlab/background_migration/redact_links/redactable.rb
new file mode 100644
index 00000000000..baab34221f1
--- /dev/null
+++ b/lib/gitlab/background_migration/redact_links/redactable.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class RedactLinks
+ module Redactable
+ extend ActiveSupport::Concern
+
+ def redact_field!(field)
+ self[field].gsub!(%r{/sent_notifications/\h{32}/unsubscribe}, '/sent_notifications/REDACTED/unsubscribe')
+
+ if self.changed?
+ self.update_columns(field => self[field],
+ "#{field}_html" => nil)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/remove_restricted_todos.rb b/lib/gitlab/background_migration/remove_restricted_todos.rb
index 9941c2fe6d9..47579d46c1b 100644
--- a/lib/gitlab/background_migration/remove_restricted_todos.rb
+++ b/lib/gitlab/background_migration/remove_restricted_todos.rb
@@ -67,7 +67,7 @@ module Gitlab
.where('access_level >= ?', 20)
confidential_issues = Issue.select(:id, :author_id).where(confidential: true, project_id: project_id)
- confidential_issues.each_batch(of: 100) do |batch|
+ confidential_issues.each_batch(of: 100, order_hint: :confidential) do |batch|
batch.each do |issue|
assigned_users = IssueAssignee.select(:user_id).where(issue_id: issue.id)
diff --git a/lib/gitlab/background_migration/set_confidential_note_events_on_services.rb b/lib/gitlab/background_migration/set_confidential_note_events_on_services.rb
index e5e8837221e..bc434b0cb64 100644
--- a/lib/gitlab/background_migration/set_confidential_note_events_on_services.rb
+++ b/lib/gitlab/background_migration/set_confidential_note_events_on_services.rb
@@ -3,8 +3,8 @@
module Gitlab
module BackgroundMigration
- # Ensures services which previously recieved all notes events continue
- # to recieve confidential ones.
+ # Ensures services which previously received all notes events continue
+ # to receive confidential ones.
class SetConfidentialNoteEventsOnServices
class Service < ActiveRecord::Base
self.table_name = 'services'
diff --git a/lib/gitlab/background_migration/set_confidential_note_events_on_webhooks.rb b/lib/gitlab/background_migration/set_confidential_note_events_on_webhooks.rb
index 171c8ef21b7..28d8d2c640b 100644
--- a/lib/gitlab/background_migration/set_confidential_note_events_on_webhooks.rb
+++ b/lib/gitlab/background_migration/set_confidential_note_events_on_webhooks.rb
@@ -3,8 +3,8 @@
module Gitlab
module BackgroundMigration
- # Ensures hooks which previously recieved all notes events continue
- # to recieve confidential ones.
+ # Ensures hooks which previously received all notes events continue
+ # to receive confidential ones.
class SetConfidentialNoteEventsOnWebhooks
class WebHook < ActiveRecord::Base
self.table_name = 'web_hooks'
diff --git a/lib/gitlab/blame.rb b/lib/gitlab/blame.rb
index 0d79594363e..f1a653a9d95 100644
--- a/lib/gitlab/blame.rb
+++ b/lib/gitlab/blame.rb
@@ -43,8 +43,7 @@ module Gitlab
def highlighted_lines
@blob.load_all_data!
- @highlighted_lines ||=
- Gitlab::Highlight.highlight(@blob.path, @blob.data, repository: repository).lines
+ @highlighted_lines ||= @blob.present.highlight.lines
end
def project
diff --git a/lib/gitlab/ci/ansi2html.rb b/lib/gitlab/ci/ansi2html.rb
index e780f8c646b..974b5ad6877 100644
--- a/lib/gitlab/ci/ansi2html.rb
+++ b/lib/gitlab/ci/ansi2html.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# ANSI color library
#
# Implementation per http://en.wikipedia.org/wiki/ANSI_escape_code
@@ -265,7 +267,7 @@ module Gitlab
def reset_state
@offset = 0
@n_open_tags = 0
- @out = ''
+ @out = +''
reset
end
diff --git a/lib/gitlab/ci/build/artifacts/adapters/gzip_stream.rb b/lib/gitlab/ci/build/artifacts/adapters/gzip_stream.rb
index ee3647f24fd..25a82086676 100644
--- a/lib/gitlab/ci/build/artifacts/adapters/gzip_stream.rb
+++ b/lib/gitlab/ci/build/artifacts/adapters/gzip_stream.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Build
diff --git a/lib/gitlab/ci/build/artifacts/adapters/raw_stream.rb b/lib/gitlab/ci/build/artifacts/adapters/raw_stream.rb
index fa6842cf36a..cf37d700991 100644
--- a/lib/gitlab/ci/build/artifacts/adapters/raw_stream.rb
+++ b/lib/gitlab/ci/build/artifacts/adapters/raw_stream.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Build
diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb
index 551d4f4473e..08dac756cc1 100644
--- a/lib/gitlab/ci/build/artifacts/metadata.rb
+++ b/lib/gitlab/ci/build/artifacts/metadata.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'zlib'
require 'json'
diff --git a/lib/gitlab/ci/build/artifacts/metadata/entry.rb b/lib/gitlab/ci/build/artifacts/metadata/entry.rb
index 85072a072d6..d0a80518ae8 100644
--- a/lib/gitlab/ci/build/artifacts/metadata/entry.rb
+++ b/lib/gitlab/ci/build/artifacts/metadata/entry.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Build
diff --git a/lib/gitlab/ci/build/artifacts/path.rb b/lib/gitlab/ci/build/artifacts/path.rb
index 9cd9b36c5f8..65cd935afaa 100644
--- a/lib/gitlab/ci/build/artifacts/path.rb
+++ b/lib/gitlab/ci/build/artifacts/path.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Build
diff --git a/lib/gitlab/ci/build/credentials/base.rb b/lib/gitlab/ci/build/credentials/base.rb
index 29a7a27c963..58adf6e506d 100644
--- a/lib/gitlab/ci/build/credentials/base.rb
+++ b/lib/gitlab/ci/build/credentials/base.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Build
diff --git a/lib/gitlab/ci/build/credentials/factory.rb b/lib/gitlab/ci/build/credentials/factory.rb
index 2423aa8857d..fa805abb8bb 100644
--- a/lib/gitlab/ci/build/credentials/factory.rb
+++ b/lib/gitlab/ci/build/credentials/factory.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Build
diff --git a/lib/gitlab/ci/build/credentials/registry.rb b/lib/gitlab/ci/build/credentials/registry.rb
index 55eafcaed10..1c8588d9913 100644
--- a/lib/gitlab/ci/build/credentials/registry.rb
+++ b/lib/gitlab/ci/build/credentials/registry.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Build
diff --git a/lib/gitlab/ci/build/image.rb b/lib/gitlab/ci/build/image.rb
index c811f88f483..4dd932f61d4 100644
--- a/lib/gitlab/ci/build/image.rb
+++ b/lib/gitlab/ci/build/image.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Build
diff --git a/lib/gitlab/ci/build/policy.rb b/lib/gitlab/ci/build/policy.rb
index d10cc7802d4..43c46ad74af 100644
--- a/lib/gitlab/ci/build/policy.rb
+++ b/lib/gitlab/ci/build/policy.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Build
diff --git a/lib/gitlab/ci/build/policy/kubernetes.rb b/lib/gitlab/ci/build/policy/kubernetes.rb
index 782f6c4c0af..4c7dc947cd0 100644
--- a/lib/gitlab/ci/build/policy/kubernetes.rb
+++ b/lib/gitlab/ci/build/policy/kubernetes.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Build
diff --git a/lib/gitlab/ci/build/policy/refs.rb b/lib/gitlab/ci/build/policy/refs.rb
index 4aa5dc89f47..10934536536 100644
--- a/lib/gitlab/ci/build/policy/refs.rb
+++ b/lib/gitlab/ci/build/policy/refs.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Build
diff --git a/lib/gitlab/ci/build/policy/specification.rb b/lib/gitlab/ci/build/policy/specification.rb
index f09ba42c074..ceb5210cfb5 100644
--- a/lib/gitlab/ci/build/policy/specification.rb
+++ b/lib/gitlab/ci/build/policy/specification.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Build
diff --git a/lib/gitlab/ci/build/policy/variables.rb b/lib/gitlab/ci/build/policy/variables.rb
index 9d2a362b7d4..0698136166a 100644
--- a/lib/gitlab/ci/build/policy/variables.rb
+++ b/lib/gitlab/ci/build/policy/variables.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Build
diff --git a/lib/gitlab/ci/build/step.rb b/lib/gitlab/ci/build/step.rb
index 0b1ebe4e048..d587c896712 100644
--- a/lib/gitlab/ci/build/step.rb
+++ b/lib/gitlab/ci/build/step.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Build
diff --git a/lib/gitlab/ci/charts.rb b/lib/gitlab/ci/charts.rb
index 7b7354bce16..a4f01468e8e 100644
--- a/lib/gitlab/ci/charts.rb
+++ b/lib/gitlab/ci/charts.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Charts
diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb
index fedaf18ef30..2fb3c4582e7 100644
--- a/lib/gitlab/ci/config.rb
+++ b/lib/gitlab/ci/config.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
#
diff --git a/lib/gitlab/ci/config/entry/artifacts.rb b/lib/gitlab/ci/config/entry/artifacts.rb
index e80f9d2e452..ef5f25b42c0 100644
--- a/lib/gitlab/ci/config/entry/artifacts.rb
+++ b/lib/gitlab/ci/config/entry/artifacts.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/attributable.rb b/lib/gitlab/ci/config/entry/attributable.rb
index 3e87a09704e..3c2e1df9b83 100644
--- a/lib/gitlab/ci/config/entry/attributable.rb
+++ b/lib/gitlab/ci/config/entry/attributable.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/boolean.rb b/lib/gitlab/ci/config/entry/boolean.rb
index f3357f85b99..b9639c83075 100644
--- a/lib/gitlab/ci/config/entry/boolean.rb
+++ b/lib/gitlab/ci/config/entry/boolean.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/cache.rb b/lib/gitlab/ci/config/entry/cache.rb
index d7e09acbbf3..0a25057f482 100644
--- a/lib/gitlab/ci/config/entry/cache.rb
+++ b/lib/gitlab/ci/config/entry/cache.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/commands.rb b/lib/gitlab/ci/config/entry/commands.rb
index 9f66f11be9b..d9658291ebe 100644
--- a/lib/gitlab/ci/config/entry/commands.rb
+++ b/lib/gitlab/ci/config/entry/commands.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/configurable.rb b/lib/gitlab/ci/config/entry/configurable.rb
index 697f622c45e..4aabf0cfa31 100644
--- a/lib/gitlab/ci/config/entry/configurable.rb
+++ b/lib/gitlab/ci/config/entry/configurable.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/coverage.rb b/lib/gitlab/ci/config/entry/coverage.rb
index 12a063059cb..690409ccf77 100644
--- a/lib/gitlab/ci/config/entry/coverage.rb
+++ b/lib/gitlab/ci/config/entry/coverage.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/environment.rb b/lib/gitlab/ci/config/entry/environment.rb
index 0c1f9eb7cbf..07e9e1d3f67 100644
--- a/lib/gitlab/ci/config/entry/environment.rb
+++ b/lib/gitlab/ci/config/entry/environment.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/factory.rb b/lib/gitlab/ci/config/entry/factory.rb
index 6be8288748f..85c9c3511a4 100644
--- a/lib/gitlab/ci/config/entry/factory.rb
+++ b/lib/gitlab/ci/config/entry/factory.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/global.rb b/lib/gitlab/ci/config/entry/global.rb
index 04077fa7a61..eba203d9d06 100644
--- a/lib/gitlab/ci/config/entry/global.rb
+++ b/lib/gitlab/ci/config/entry/global.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/hidden.rb b/lib/gitlab/ci/config/entry/hidden.rb
index 6fc3aa385bc..dc0ede2a25f 100644
--- a/lib/gitlab/ci/config/entry/hidden.rb
+++ b/lib/gitlab/ci/config/entry/hidden.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/image.rb b/lib/gitlab/ci/config/entry/image.rb
index 2844be80a84..fc453b72fa5 100644
--- a/lib/gitlab/ci/config/entry/image.rb
+++ b/lib/gitlab/ci/config/entry/image.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index f290ff3a565..c8cb3248fa7 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
@@ -12,7 +14,7 @@ module Gitlab
ALLOWED_KEYS = %i[tags script only except type image services
allow_failure type stage when start_in artifacts cache
dependencies before_script after_script variables
- environment coverage retry extends].freeze
+ environment coverage retry parallel extends].freeze
validations do
validates :config, allowed_keys: ALLOWED_KEYS
@@ -24,14 +26,13 @@ module Gitlab
with_options allow_nil: true do
validates :tags, array_of_strings: true
validates :allow_failure, boolean: true
- validates :retry, numericality: { only_integer: true,
- greater_than_or_equal_to: 0,
- less_than_or_equal_to: 2 }
+ validates :parallel, numericality: { only_integer: true,
+ greater_than_or_equal_to: 2,
+ less_than_or_equal_to: 50 }
validates :when,
inclusion: { in: %w[on_success on_failure always manual delayed],
message: 'should be on_success, on_failure, ' \
'always, manual or delayed' }
-
validates :dependencies, array_of_strings: true
validates :extends, type: String
end
@@ -77,17 +78,21 @@ module Gitlab
description: 'Artifacts configuration for this job.'
entry :environment, Entry::Environment,
- description: 'Environment configuration for this job.'
+ description: 'Environment configuration for this job.'
entry :coverage, Entry::Coverage,
- description: 'Coverage configuration for this job.'
+ description: 'Coverage configuration for this job.'
+
+ entry :retry, Entry::Retry,
+ description: 'Retry configuration for this job.'
helpers :before_script, :script, :stage, :type, :after_script,
:cache, :image, :services, :only, :except, :variables,
- :artifacts, :commands, :environment, :coverage, :retry
+ :artifacts, :commands, :environment, :coverage, :retry,
+ :parallel
attributes :script, :tags, :allow_failure, :when, :dependencies,
- :retry, :extends, :start_in
+ :retry, :parallel, :extends, :start_in
def compose!(deps = nil)
super do
@@ -155,7 +160,8 @@ module Gitlab
environment: environment_defined? ? environment_value : nil,
environment_name: environment_defined? ? environment_value[:name] : nil,
coverage: coverage_defined? ? coverage_value : nil,
- retry: retry_defined? ? retry_value.to_i : nil,
+ retry: retry_defined? ? retry_value : nil,
+ parallel: parallel_defined? ? parallel_value.to_i : nil,
artifacts: artifacts_value,
after_script: after_script_value,
ignore: ignored? }
diff --git a/lib/gitlab/ci/config/entry/jobs.rb b/lib/gitlab/ci/config/entry/jobs.rb
index 96b6f2e5d6c..1535b108000 100644
--- a/lib/gitlab/ci/config/entry/jobs.rb
+++ b/lib/gitlab/ci/config/entry/jobs.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/key.rb b/lib/gitlab/ci/config/entry/key.rb
index f27ad0a7759..963b200c7bb 100644
--- a/lib/gitlab/ci/config/entry/key.rb
+++ b/lib/gitlab/ci/config/entry/key.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb
index a3d4432be82..4043629dea9 100644
--- a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb
+++ b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/node.rb b/lib/gitlab/ci/config/entry/node.rb
index 26505c91be3..347089722e4 100644
--- a/lib/gitlab/ci/config/entry/node.rb
+++ b/lib/gitlab/ci/config/entry/node.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/paths.rb b/lib/gitlab/ci/config/entry/paths.rb
index 68dad161149..9580b5e2e7f 100644
--- a/lib/gitlab/ci/config/entry/paths.rb
+++ b/lib/gitlab/ci/config/entry/paths.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/policy.rb b/lib/gitlab/ci/config/entry/policy.rb
index c92562f8c85..0535d7c1a1a 100644
--- a/lib/gitlab/ci/config/entry/policy.rb
+++ b/lib/gitlab/ci/config/entry/policy.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/retry.rb b/lib/gitlab/ci/config/entry/retry.rb
new file mode 100644
index 00000000000..e39cc5de229
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/retry.rb
@@ -0,0 +1,90 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents a retry config for a job.
+ #
+ class Retry < Simplifiable
+ strategy :SimpleRetry, if: -> (config) { config.is_a?(Integer) }
+ strategy :FullRetry, if: -> (config) { config.is_a?(Hash) }
+
+ class SimpleRetry < Entry::Node
+ include Entry::Validatable
+
+ validations do
+ validates :config, numericality: { only_integer: true,
+ greater_than_or_equal_to: 0,
+ less_than_or_equal_to: 2 }
+ end
+
+ def value
+ {
+ max: config
+ }
+ end
+
+ def location
+ 'retry'
+ end
+ end
+
+ class FullRetry < Entry::Node
+ include Entry::Validatable
+ include Entry::Attributable
+
+ ALLOWED_KEYS = %i[max when].freeze
+ attributes :max, :when
+
+ validations do
+ validates :config, allowed_keys: ALLOWED_KEYS
+
+ with_options allow_nil: true do
+ validates :max, numericality: { only_integer: true,
+ greater_than_or_equal_to: 0,
+ less_than_or_equal_to: 2 }
+
+ validates :when, array_of_strings_or_string: true
+ validates :when,
+ allowed_array_values: { in: FullRetry.possible_retry_when_values },
+ if: -> (config) { config.when.is_a?(Array) }
+ validates :when,
+ inclusion: { in: FullRetry.possible_retry_when_values },
+ if: -> (config) { config.when.is_a?(String) }
+ end
+ end
+
+ def self.possible_retry_when_values
+ @possible_retry_when_values ||= ::Ci::Build.failure_reasons.keys.map(&:to_s) + ['always']
+ end
+
+ def value
+ super.tap do |config|
+ # make sure that `when` is an array, because we allow it to
+ # be passed as a String in config for simplicity
+ config[:when] = Array.wrap(config[:when]) if config[:when]
+ end
+ end
+
+ def location
+ 'retry'
+ end
+ end
+
+ class UnknownStrategy < Entry::Node
+ def errors
+ ["#{location} has to be either an integer or a hash"]
+ end
+
+ def location
+ 'retry config'
+ end
+ end
+
+ def self.default
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/script.rb b/lib/gitlab/ci/config/entry/script.rb
index 29ecd9995ca..f7d39e5cf55 100644
--- a/lib/gitlab/ci/config/entry/script.rb
+++ b/lib/gitlab/ci/config/entry/script.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/service.rb b/lib/gitlab/ci/config/entry/service.rb
index 3e2ebcff31a..47bf9205147 100644
--- a/lib/gitlab/ci/config/entry/service.rb
+++ b/lib/gitlab/ci/config/entry/service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/services.rb b/lib/gitlab/ci/config/entry/services.rb
index 0066894e069..bdf7f80f382 100644
--- a/lib/gitlab/ci/config/entry/services.rb
+++ b/lib/gitlab/ci/config/entry/services.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/simplifiable.rb b/lib/gitlab/ci/config/entry/simplifiable.rb
index 12764629686..9961bbfaa40 100644
--- a/lib/gitlab/ci/config/entry/simplifiable.rb
+++ b/lib/gitlab/ci/config/entry/simplifiable.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/stage.rb b/lib/gitlab/ci/config/entry/stage.rb
index b7afaba1de8..65ab5953131 100644
--- a/lib/gitlab/ci/config/entry/stage.rb
+++ b/lib/gitlab/ci/config/entry/stage.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/stages.rb b/lib/gitlab/ci/config/entry/stages.rb
index ec187bd3732..ab184246d29 100644
--- a/lib/gitlab/ci/config/entry/stages.rb
+++ b/lib/gitlab/ci/config/entry/stages.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/undefined.rb b/lib/gitlab/ci/config/entry/undefined.rb
index 1171ac10f22..77dcfa88170 100644
--- a/lib/gitlab/ci/config/entry/undefined.rb
+++ b/lib/gitlab/ci/config/entry/undefined.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/unspecified.rb b/lib/gitlab/ci/config/entry/unspecified.rb
index fbb2551e870..bab32489d2f 100644
--- a/lib/gitlab/ci/config/entry/unspecified.rb
+++ b/lib/gitlab/ci/config/entry/unspecified.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/validatable.rb b/lib/gitlab/ci/config/entry/validatable.rb
index e45787773a8..08a6593c980 100644
--- a/lib/gitlab/ci/config/entry/validatable.rb
+++ b/lib/gitlab/ci/config/entry/validatable.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/validator.rb b/lib/gitlab/ci/config/entry/validator.rb
index 2df23a3edcd..33ffdd3a95d 100644
--- a/lib/gitlab/ci/config/entry/validator.rb
+++ b/lib/gitlab/ci/config/entry/validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb
index f6b4ba7843e..a1d552fb2e5 100644
--- a/lib/gitlab/ci/config/entry/validators.rb
+++ b/lib/gitlab/ci/config/entry/validators.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
@@ -5,11 +7,11 @@ module Gitlab
module Validators
class AllowedKeysValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
- unknown_keys = record.config.try(:keys).to_a - options[:in]
+ unknown_keys = value.try(:keys).to_a - options[:in]
if unknown_keys.any?
- record.errors.add(:config, 'contains unknown keys: ' +
- unknown_keys.join(', '))
+ record.errors.add(attribute, "contains unknown keys: " +
+ unknown_keys.join(', '))
end
end
end
@@ -22,6 +24,16 @@ module Gitlab
end
end
+ class AllowedArrayValuesValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unkown_values = value - options[:in]
+ unless unkown_values.empty?
+ record.errors.add(attribute, "contains unknown values: " +
+ unkown_values.join(', '))
+ end
+ end
+ end
+
class ArrayOfStringsValidator < ActiveModel::EachValidator
include LegacyValidationHelpers
@@ -66,6 +78,14 @@ module Gitlab
end
end
+ class HashOrIntegerValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unless value.is_a?(Hash) || value.is_a?(Integer)
+ record.errors.add(attribute, 'should be a hash or an integer')
+ end
+ end
+ end
+
class KeyValidator < ActiveModel::EachValidator
include LegacyValidationHelpers
diff --git a/lib/gitlab/ci/config/entry/variables.rb b/lib/gitlab/ci/config/entry/variables.rb
index 8acab605c91..6fd3cec2f5f 100644
--- a/lib/gitlab/ci/config/entry/variables.rb
+++ b/lib/gitlab/ci/config/entry/variables.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/loader.rb b/lib/gitlab/ci/config/loader.rb
index 141d2714cb6..b4c491e84a6 100644
--- a/lib/gitlab/ci/config/loader.rb
+++ b/lib/gitlab/ci/config/loader.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/normalizer.rb b/lib/gitlab/ci/config/normalizer.rb
new file mode 100644
index 00000000000..b7743bd2090
--- /dev/null
+++ b/lib/gitlab/ci/config/normalizer.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ class Normalizer
+ def initialize(jobs_config)
+ @jobs_config = jobs_config
+ end
+
+ def normalize_jobs
+ extract_parallelized_jobs!
+ return @jobs_config if @parallelized_jobs.empty?
+
+ parallelized_config = parallelize_jobs
+ parallelize_dependencies(parallelized_config)
+ end
+
+ private
+
+ def extract_parallelized_jobs!
+ @parallelized_jobs = {}
+
+ @jobs_config.each do |job_name, config|
+ if config[:parallel]
+ @parallelized_jobs[job_name] = self.class.parallelize_job_names(job_name, config[:parallel])
+ end
+ end
+
+ @parallelized_jobs
+ end
+
+ def parallelize_jobs
+ @jobs_config.each_with_object({}) do |(job_name, config), hash|
+ if @parallelized_jobs.key?(job_name)
+ @parallelized_jobs[job_name].each { |name, index| hash[name.to_sym] = config.merge(name: name, instance: index) }
+ else
+ hash[job_name] = config
+ end
+
+ hash
+ end
+ end
+
+ def parallelize_dependencies(parallelized_config)
+ parallelized_job_names = @parallelized_jobs.keys.map(&:to_s)
+ parallelized_config.each_with_object({}) do |(job_name, config), hash|
+ if config[:dependencies] && (intersection = config[:dependencies] & parallelized_job_names).any?
+ deps = intersection.map { |dep| @parallelized_jobs[dep.to_sym].map(&:first) }.flatten
+ hash[job_name] = config.merge(dependencies: deps)
+ else
+ hash[job_name] = config
+ end
+
+ hash
+ end
+ end
+
+ def self.parallelize_job_names(name, total)
+ Array.new(total) { |index| ["#{name} #{index + 1}/#{total}", index + 1] }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/cron_parser.rb b/lib/gitlab/ci/cron_parser.rb
index 73f36735e35..b1db9084662 100644
--- a/lib/gitlab/ci/cron_parser.rb
+++ b/lib/gitlab/ci/cron_parser.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class CronParser
diff --git a/lib/gitlab/ci/mask_secret.rb b/lib/gitlab/ci/mask_secret.rb
index 0daddaa638c..58d55b1bd6f 100644
--- a/lib/gitlab/ci/mask_secret.rb
+++ b/lib/gitlab/ci/mask_secret.rb
@@ -1,9 +1,13 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci::MaskSecret
class << self
def mask!(value, token)
return value unless value.present? && token.present?
+ # We assume 'value' must be mutable, given
+ # that frozen string is enabled.
value.gsub!(token, 'x' * token.length)
value
end
diff --git a/lib/gitlab/ci/model.rb b/lib/gitlab/ci/model.rb
index 3994a50772b..fbdb84c0522 100644
--- a/lib/gitlab/ci/model.rb
+++ b/lib/gitlab/ci/model.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Model
diff --git a/lib/gitlab/ci/pipeline/chain/base.rb b/lib/gitlab/ci/pipeline/chain/base.rb
index efed19da21c..bab1c73e2f1 100644
--- a/lib/gitlab/ci/pipeline/chain/base.rb
+++ b/lib/gitlab/ci/pipeline/chain/base.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/chain/build.rb b/lib/gitlab/ci/pipeline/chain/build.rb
index b5eb0cfa2f0..b445a872b3d 100644
--- a/lib/gitlab/ci/pipeline/chain/build.rb
+++ b/lib/gitlab/ci/pipeline/chain/build.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb
index a53c80d34f7..05978804d92 100644
--- a/lib/gitlab/ci/pipeline/chain/command.rb
+++ b/lib/gitlab/ci/pipeline/chain/command.rb
@@ -1,4 +1,7 @@
-module Gitlab # rubocop:disable Naming/FileName
+# rubocop:disable Naming/FileName
+# frozen_string_literal: true
+
+module Gitlab
module Ci
module Pipeline
module Chain
diff --git a/lib/gitlab/ci/pipeline/chain/create.rb b/lib/gitlab/ci/pipeline/chain/create.rb
index 02493c7fe02..aa627bdb009 100644
--- a/lib/gitlab/ci/pipeline/chain/create.rb
+++ b/lib/gitlab/ci/pipeline/chain/create.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
@@ -5,26 +7,11 @@ module Gitlab
class Create < Chain::Base
include Chain::Helpers
- # rubocop: disable CodeReuse/ActiveRecord
def perform!
- ::Ci::Pipeline.transaction do
- pipeline.save!
-
- ##
- # Create environments before the pipeline starts.
- #
- pipeline.builds.each do |build|
- if build.has_environment?
- project.environments.find_or_create_by(
- name: build.expanded_environment_name
- )
- end
- end
- end
+ pipeline.save!
rescue ActiveRecord::RecordInvalid => e
error("Failed to persist the pipeline: #{e}")
end
- # rubocop: enable CodeReuse/ActiveRecord
def break?
!pipeline.persisted?
diff --git a/lib/gitlab/ci/pipeline/chain/helpers.rb b/lib/gitlab/ci/pipeline/chain/helpers.rb
index bf1380a1da9..6bb3a75291b 100644
--- a/lib/gitlab/ci/pipeline/chain/helpers.rb
+++ b/lib/gitlab/ci/pipeline/chain/helpers.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/chain/populate.rb b/lib/gitlab/ci/pipeline/chain/populate.rb
index f34c11ca3c2..633d3cd4f6b 100644
--- a/lib/gitlab/ci/pipeline/chain/populate.rb
+++ b/lib/gitlab/ci/pipeline/chain/populate.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/chain/sequence.rb b/lib/gitlab/ci/pipeline/chain/sequence.rb
index e24630656d3..99780409085 100644
--- a/lib/gitlab/ci/pipeline/chain/sequence.rb
+++ b/lib/gitlab/ci/pipeline/chain/sequence.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/chain/skip.rb b/lib/gitlab/ci/pipeline/chain/skip.rb
index 32cbb7ca6af..b9707d2f8f5 100644
--- a/lib/gitlab/ci/pipeline/chain/skip.rb
+++ b/lib/gitlab/ci/pipeline/chain/skip.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/chain/validate/abilities.rb b/lib/gitlab/ci/pipeline/chain/validate/abilities.rb
index 13c6fedd831..ebd7e6e8289 100644
--- a/lib/gitlab/ci/pipeline/chain/validate/abilities.rb
+++ b/lib/gitlab/ci/pipeline/chain/validate/abilities.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/chain/validate/config.rb b/lib/gitlab/ci/pipeline/chain/validate/config.rb
index a3bd2a5a23a..28c38cc3d18 100644
--- a/lib/gitlab/ci/pipeline/chain/validate/config.rb
+++ b/lib/gitlab/ci/pipeline/chain/validate/config.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/chain/validate/repository.rb b/lib/gitlab/ci/pipeline/chain/validate/repository.rb
index 9699c24e5b6..d88851d8245 100644
--- a/lib/gitlab/ci/pipeline/chain/validate/repository.rb
+++ b/lib/gitlab/ci/pipeline/chain/validate/repository.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/duration.rb b/lib/gitlab/ci/pipeline/duration.rb
index 30701e1de1b..de24bbf688b 100644
--- a/lib/gitlab/ci/pipeline/duration.rb
+++ b/lib/gitlab/ci/pipeline/duration.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/expression.rb b/lib/gitlab/ci/pipeline/expression.rb
index f57df7c5637..61d392121d8 100644
--- a/lib/gitlab/ci/pipeline/expression.rb
+++ b/lib/gitlab/ci/pipeline/expression.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/base.rb b/lib/gitlab/ci/pipeline/expression/lexeme/base.rb
index 047ab66e9b3..70c774416f6 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/base.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/base.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/equals.rb b/lib/gitlab/ci/pipeline/expression/lexeme/equals.rb
index 3a2f0c6924e..668e85f5b9e 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/equals.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/equals.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb b/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb
index 10957598f76..cd17bc4d78b 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/null.rb b/lib/gitlab/ci/pipeline/expression/lexeme/null.rb
index a2778716924..be7258c201a 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/null.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/null.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/operator.rb b/lib/gitlab/ci/pipeline/expression/lexeme/operator.rb
index f640d0b5855..3ebceb92eb7 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/operator.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/operator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb b/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb
index 9b239c29ea4..d7e6dacf068 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/string.rb b/lib/gitlab/ci/pipeline/expression/lexeme/string.rb
index 346c92dc51e..2db2bf011f1 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/string.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/string.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/value.rb b/lib/gitlab/ci/pipeline/expression/lexeme/value.rb
index f2611d65faf..ef9ddb6cae9 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/value.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/value.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/variable.rb b/lib/gitlab/ci/pipeline/expression/lexeme/variable.rb
index 37643c8ef53..85c0899e4f6 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/variable.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/variable.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/expression/lexer.rb b/lib/gitlab/ci/pipeline/expression/lexer.rb
index 4cacb1e62c9..f26542361a2 100644
--- a/lib/gitlab/ci/pipeline/expression/lexer.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/expression/parser.rb b/lib/gitlab/ci/pipeline/expression/parser.rb
index 90f94d0b763..ed184309ab4 100644
--- a/lib/gitlab/ci/pipeline/expression/parser.rb
+++ b/lib/gitlab/ci/pipeline/expression/parser.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/expression/statement.rb b/lib/gitlab/ci/pipeline/expression/statement.rb
index b36f1e0f865..b03611f756e 100644
--- a/lib/gitlab/ci/pipeline/expression/statement.rb
+++ b/lib/gitlab/ci/pipeline/expression/statement.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/expression/token.rb b/lib/gitlab/ci/pipeline/expression/token.rb
index 58211800b88..513d43f6fca 100644
--- a/lib/gitlab/ci/pipeline/expression/token.rb
+++ b/lib/gitlab/ci/pipeline/expression/token.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/seed/base.rb b/lib/gitlab/ci/pipeline/seed/base.rb
index db9706924bb..1fd3a61017f 100644
--- a/lib/gitlab/ci/pipeline/seed/base.rb
+++ b/lib/gitlab/ci/pipeline/seed/base.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb
index 6980b0b7aff..ef738a93bfe 100644
--- a/lib/gitlab/ci/pipeline/seed/build.rb
+++ b/lib/gitlab/ci/pipeline/seed/build.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/seed/stage.rb b/lib/gitlab/ci/pipeline/seed/stage.rb
index 2b58d9863a0..4775ff15581 100644
--- a/lib/gitlab/ci/pipeline/seed/stage.rb
+++ b/lib/gitlab/ci/pipeline/seed/stage.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/reports/test_case.rb b/lib/gitlab/ci/reports/test_case.rb
index b4d08ed257f..292e273a03a 100644
--- a/lib/gitlab/ci/reports/test_case.rb
+++ b/lib/gitlab/ci/reports/test_case.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Reports
diff --git a/lib/gitlab/ci/reports/test_reports.rb b/lib/gitlab/ci/reports/test_reports.rb
index c87bdb4a8a2..7397ff35d46 100644
--- a/lib/gitlab/ci/reports/test_reports.rb
+++ b/lib/gitlab/ci/reports/test_reports.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Reports
diff --git a/lib/gitlab/ci/reports/test_reports_comparer.rb b/lib/gitlab/ci/reports/test_reports_comparer.rb
index 726c6a11a81..11810bdc0a8 100644
--- a/lib/gitlab/ci/reports/test_reports_comparer.rb
+++ b/lib/gitlab/ci/reports/test_reports_comparer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Reports
diff --git a/lib/gitlab/ci/reports/test_suite.rb b/lib/gitlab/ci/reports/test_suite.rb
index b5f15397c0f..b0391160c15 100644
--- a/lib/gitlab/ci/reports/test_suite.rb
+++ b/lib/gitlab/ci/reports/test_suite.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Reports
diff --git a/lib/gitlab/ci/reports/test_suite_comparer.rb b/lib/gitlab/ci/reports/test_suite_comparer.rb
index 642aa593092..9cb7db5934c 100644
--- a/lib/gitlab/ci/reports/test_suite_comparer.rb
+++ b/lib/gitlab/ci/reports/test_suite_comparer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Reports
diff --git a/lib/gitlab/ci/status/build/action.rb b/lib/gitlab/ci/status/build/action.rb
index 6c9125647ad..45d9ba41e92 100644
--- a/lib/gitlab/ci/status/build/action.rb
+++ b/lib/gitlab/ci/status/build/action.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb
index 024047d4983..43fb5cdbbe6 100644
--- a/lib/gitlab/ci/status/build/cancelable.rb
+++ b/lib/gitlab/ci/status/build/cancelable.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/canceled.rb b/lib/gitlab/ci/status/build/canceled.rb
index c83e2734a73..0518b9e673d 100644
--- a/lib/gitlab/ci/status/build/canceled.rb
+++ b/lib/gitlab/ci/status/build/canceled.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/common.rb b/lib/gitlab/ci/status/build/common.rb
index c1fc70ac266..6a75ec5c37f 100644
--- a/lib/gitlab/ci/status/build/common.rb
+++ b/lib/gitlab/ci/status/build/common.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/created.rb b/lib/gitlab/ci/status/build/created.rb
index 5be8e9de425..780fea23123 100644
--- a/lib/gitlab/ci/status/build/created.rb
+++ b/lib/gitlab/ci/status/build/created.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/erased.rb b/lib/gitlab/ci/status/build/erased.rb
index 495227c2ffb..d74cfc1ee77 100644
--- a/lib/gitlab/ci/status/build/erased.rb
+++ b/lib/gitlab/ci/status/build/erased.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb
index 4a74d6d6ed1..6e4bfe23f2b 100644
--- a/lib/gitlab/ci/status/build/factory.rb
+++ b/lib/gitlab/ci/status/build/factory.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/failed.rb b/lib/gitlab/ci/status/build/failed.rb
index 4babc23a495..d40454df737 100644
--- a/lib/gitlab/ci/status/build/failed.rb
+++ b/lib/gitlab/ci/status/build/failed.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
@@ -12,7 +14,8 @@ module Gitlab
missing_dependency_failure: 'missing dependency failure',
runner_unsupported: 'unsupported runner',
stale_schedule: 'stale schedule',
- job_execution_timeout: 'job execution timeout'
+ job_execution_timeout: 'job execution timeout',
+ archived_failure: 'archived failure'
}.freeze
private_constant :REASONS
diff --git a/lib/gitlab/ci/status/build/failed_allowed.rb b/lib/gitlab/ci/status/build/failed_allowed.rb
index ca0046fb1f7..d7570fdd3e2 100644
--- a/lib/gitlab/ci/status/build/failed_allowed.rb
+++ b/lib/gitlab/ci/status/build/failed_allowed.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/manual.rb b/lib/gitlab/ci/status/build/manual.rb
index 042da6392d3..d01b09f1398 100644
--- a/lib/gitlab/ci/status/build/manual.rb
+++ b/lib/gitlab/ci/status/build/manual.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/pending.rb b/lib/gitlab/ci/status/build/pending.rb
index 9dd9a27ad57..95f668295dd 100644
--- a/lib/gitlab/ci/status/build/pending.rb
+++ b/lib/gitlab/ci/status/build/pending.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb
index a8b9ebf0803..c66b8ca5654 100644
--- a/lib/gitlab/ci/status/build/play.rb
+++ b/lib/gitlab/ci/status/build/play.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/retried.rb b/lib/gitlab/ci/status/build/retried.rb
index 6e190e4ee3c..b489dc68733 100644
--- a/lib/gitlab/ci/status/build/retried.rb
+++ b/lib/gitlab/ci/status/build/retried.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb
index 5aeb8e51480..eb6b3f21604 100644
--- a/lib/gitlab/ci/status/build/retryable.rb
+++ b/lib/gitlab/ci/status/build/retryable.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/scheduled.rb b/lib/gitlab/ci/status/build/scheduled.rb
index 62ad9083616..b3452eae189 100644
--- a/lib/gitlab/ci/status/build/scheduled.rb
+++ b/lib/gitlab/ci/status/build/scheduled.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
@@ -7,7 +9,7 @@ module Gitlab
{
image: 'illustrations/illustrations_scheduled-job_countdown.svg',
size: 'svg-394',
- title: _("This is a delayed to run in ") + " #{execute_in}",
+ title: _("This is a delayed job to run in %{remainingTime}"),
content: _("This job will automatically run after it's timer finishes. " \
"Often they are used for incremental roll-out deploys " \
"to production environments. When unscheduled it converts " \
@@ -16,21 +18,12 @@ module Gitlab
end
def status_tooltip
- "delayed manual action (#{execute_in})"
+ "delayed manual action (%{remainingTime})"
end
def self.matches?(build, user)
build.scheduled? && build.scheduled_at
end
-
- private
-
- include TimeHelper
-
- def execute_in
- remaining_seconds = [0, subject.scheduled_at - Time.now].max
- duration_in_numbers(remaining_seconds)
- end
end
end
end
diff --git a/lib/gitlab/ci/status/build/skipped.rb b/lib/gitlab/ci/status/build/skipped.rb
index 3e678d0baee..4fe2f7b3114 100644
--- a/lib/gitlab/ci/status/build/skipped.rb
+++ b/lib/gitlab/ci/status/build/skipped.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb
index dea838bfa39..a620e7ad126 100644
--- a/lib/gitlab/ci/status/build/stop.rb
+++ b/lib/gitlab/ci/status/build/stop.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/unschedule.rb b/lib/gitlab/ci/status/build/unschedule.rb
index e1b7b83428c..9110839cb55 100644
--- a/lib/gitlab/ci/status/build/unschedule.rb
+++ b/lib/gitlab/ci/status/build/unschedule.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/canceled.rb b/lib/gitlab/ci/status/canceled.rb
index e6195a60d4f..07f37732023 100644
--- a/lib/gitlab/ci/status/canceled.rb
+++ b/lib/gitlab/ci/status/canceled.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb
index 9d6a2f51c11..ea773ee9944 100644
--- a/lib/gitlab/ci/status/core.rb
+++ b/lib/gitlab/ci/status/core.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/created.rb b/lib/gitlab/ci/status/created.rb
index 846f00b83dd..fface4bb97b 100644
--- a/lib/gitlab/ci/status/created.rb
+++ b/lib/gitlab/ci/status/created.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/extended.rb b/lib/gitlab/ci/status/extended.rb
index 1e8101f8949..b72a28ed0b6 100644
--- a/lib/gitlab/ci/status/extended.rb
+++ b/lib/gitlab/ci/status/extended.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/external/common.rb b/lib/gitlab/ci/status/external/common.rb
index 9307545b5b1..4169f5b3210 100644
--- a/lib/gitlab/ci/status/external/common.rb
+++ b/lib/gitlab/ci/status/external/common.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/external/factory.rb b/lib/gitlab/ci/status/external/factory.rb
index 07b15bd8d97..91fafb940a8 100644
--- a/lib/gitlab/ci/status/external/factory.rb
+++ b/lib/gitlab/ci/status/external/factory.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/factory.rb b/lib/gitlab/ci/status/factory.rb
index 15836c699c7..3446644eff8 100644
--- a/lib/gitlab/ci/status/factory.rb
+++ b/lib/gitlab/ci/status/factory.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/failed.rb b/lib/gitlab/ci/status/failed.rb
index 27ce85bd3ed..770ed7d4d5a 100644
--- a/lib/gitlab/ci/status/failed.rb
+++ b/lib/gitlab/ci/status/failed.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/group/common.rb b/lib/gitlab/ci/status/group/common.rb
index cfd4329a923..0b5ea0712ca 100644
--- a/lib/gitlab/ci/status/group/common.rb
+++ b/lib/gitlab/ci/status/group/common.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/group/factory.rb b/lib/gitlab/ci/status/group/factory.rb
index d118116cfc3..ee785856fdd 100644
--- a/lib/gitlab/ci/status/group/factory.rb
+++ b/lib/gitlab/ci/status/group/factory.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/manual.rb b/lib/gitlab/ci/status/manual.rb
index fc387e2fd25..50c92add400 100644
--- a/lib/gitlab/ci/status/manual.rb
+++ b/lib/gitlab/ci/status/manual.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/pending.rb b/lib/gitlab/ci/status/pending.rb
index 6780780db32..cea7e6ed938 100644
--- a/lib/gitlab/ci/status/pending.rb
+++ b/lib/gitlab/ci/status/pending.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/pipeline/blocked.rb b/lib/gitlab/ci/status/pipeline/blocked.rb
index bf7e484ee9b..ed13a439be0 100644
--- a/lib/gitlab/ci/status/pipeline/blocked.rb
+++ b/lib/gitlab/ci/status/pipeline/blocked.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/pipeline/common.rb b/lib/gitlab/ci/status/pipeline/common.rb
index 61bb07beb0f..7b34a2ea858 100644
--- a/lib/gitlab/ci/status/pipeline/common.rb
+++ b/lib/gitlab/ci/status/pipeline/common.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/pipeline/delayed.rb b/lib/gitlab/ci/status/pipeline/delayed.rb
index 12736861c89..e61acdcd167 100644
--- a/lib/gitlab/ci/status/pipeline/delayed.rb
+++ b/lib/gitlab/ci/status/pipeline/delayed.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/pipeline/factory.rb b/lib/gitlab/ci/status/pipeline/factory.rb
index 0adf83fa197..5d1a8bbd924 100644
--- a/lib/gitlab/ci/status/pipeline/factory.rb
+++ b/lib/gitlab/ci/status/pipeline/factory.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/running.rb b/lib/gitlab/ci/status/running.rb
index ee13905e46d..ac7dd74cdce 100644
--- a/lib/gitlab/ci/status/running.rb
+++ b/lib/gitlab/ci/status/running.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/scheduled.rb b/lib/gitlab/ci/status/scheduled.rb
index 3adcfa36af2..16ad1da89e3 100644
--- a/lib/gitlab/ci/status/scheduled.rb
+++ b/lib/gitlab/ci/status/scheduled.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/skipped.rb b/lib/gitlab/ci/status/skipped.rb
index 0dbdc4de426..aaec1e1d201 100644
--- a/lib/gitlab/ci/status/skipped.rb
+++ b/lib/gitlab/ci/status/skipped.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/stage/common.rb b/lib/gitlab/ci/status/stage/common.rb
index f60a7662075..f12daaa9676 100644
--- a/lib/gitlab/ci/status/stage/common.rb
+++ b/lib/gitlab/ci/status/stage/common.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/stage/factory.rb b/lib/gitlab/ci/status/stage/factory.rb
index 4c37f084d07..58f4642510b 100644
--- a/lib/gitlab/ci/status/stage/factory.rb
+++ b/lib/gitlab/ci/status/stage/factory.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/success.rb b/lib/gitlab/ci/status/success.rb
index 731013ec017..020f2c5b89f 100644
--- a/lib/gitlab/ci/status/success.rb
+++ b/lib/gitlab/ci/status/success.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/success_warning.rb b/lib/gitlab/ci/status/success_warning.rb
index 32b4cf43e48..6632cd9b143 100644
--- a/lib/gitlab/ci/status/success_warning.rb
+++ b/lib/gitlab/ci/status/success_warning.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
index db48b187e5e..149506ea498 100644
--- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
@@ -27,7 +27,7 @@
# Continuous deployment to production is enabled by default.
# If you want to deploy to staging first, set STAGING_ENABLED environment variable.
# If you want to enable incremental rollout, either manual or time based,
-# set INCREMENTAL_ROLLOUT_TYPE environment variable to "manual" or "timed".
+# set INCREMENTAL_ROLLOUT_MODE environment variable to "manual" or "timed".
# If you want to use canary deployments, set CANARY_ENABLED environment variable.
#
# If Auto DevOps fails to detect the proper buildpack, or if you want to
@@ -49,7 +49,7 @@ variables:
POSTGRES_ENABLED: "true"
POSTGRES_DB: $CI_ENVIRONMENT_SLUG
- KUBERNETES_VERSION: 1.8.6
+ KUBERNETES_VERSION: 1.10.9
HELM_VERSION: 2.11.0
DOCKER_DRIVER: overlay2
@@ -116,12 +116,11 @@ code_quality:
license_management:
stage: test
- image: docker:stable
+ image:
+ name: "registry.gitlab.com/gitlab-org/security-products/license-management:$CI_SERVER_VERSION_MAJOR-$CI_SERVER_VERSION_MINOR-stable"
+ entrypoint: [""]
allow_failure: true
- services:
- - docker:stable-dind
script:
- - setup_docker
- license_management
artifacts:
paths: [gl-license-management-report.json]
@@ -150,10 +149,10 @@ performance:
only:
refs:
- branches
- kubernetes: active
except:
variables:
- $PERFORMANCE_DISABLED
+ - $KUBECONFIG == null
sast:
stage: test
@@ -228,7 +227,6 @@ dast:
only:
refs:
- branches
- kubernetes: active
variables:
- $GITLAB_FEATURES =~ /\bdast\b/
except:
@@ -236,6 +234,7 @@ dast:
- master
variables:
- $DAST_DISABLED
+ - $KUBECONFIG == null
review:
stage: review
@@ -257,12 +256,12 @@ review:
only:
refs:
- branches
- kubernetes: active
except:
refs:
- master
variables:
- $REVIEW_DISABLED
+ - $KUBECONFIG == null
stop_review:
stage: cleanup
@@ -280,12 +279,12 @@ stop_review:
only:
refs:
- branches
- kubernetes: active
except:
refs:
- master
variables:
- $REVIEW_DISABLED
+ - $KUBECONFIG == null
# Staging deploys are disabled by default since
# continuous deployment to production is enabled by default
@@ -309,9 +308,11 @@ staging:
only:
refs:
- master
- kubernetes: active
variables:
- $STAGING_ENABLED
+ except:
+ variables:
+ - $KUBECONFIG == null
# Canaries are also disabled by default, but if you want them,
# and know what the downsides are, you can enable this by setting
@@ -334,9 +335,11 @@ canary:
only:
refs:
- master
- kubernetes: active
variables:
- $CANARY_ENABLED
+ except:
+ variables:
+ - $KUBECONFIG == null
.production: &production_template
stage: production
@@ -362,13 +365,13 @@ production:
only:
refs:
- master
- kubernetes: active
except:
variables:
- $STAGING_ENABLED
- $CANARY_ENABLED
- $INCREMENTAL_ROLLOUT_ENABLED
- $INCREMENTAL_ROLLOUT_MODE
+ - $KUBECONFIG == null
production_manual:
<<: *production_template
@@ -377,7 +380,6 @@ production_manual:
only:
refs:
- master
- kubernetes: active
variables:
- $STAGING_ENABLED
- $CANARY_ENABLED
@@ -385,6 +387,7 @@ production_manual:
variables:
- $INCREMENTAL_ROLLOUT_ENABLED
- $INCREMENTAL_ROLLOUT_MODE
+ - $KUBECONFIG == null
# This job implements incremental rollout on for every push to `master`.
@@ -414,13 +417,13 @@ production_manual:
only:
refs:
- master
- kubernetes: active
variables:
- $INCREMENTAL_ROLLOUT_MODE == "manual"
- $INCREMENTAL_ROLLOUT_ENABLED
except:
variables:
- $INCREMENTAL_ROLLOUT_MODE == "timed"
+ - $KUBECONFIG == null
.timed_rollout_template: &timed_rollout_template
<<: *rollout_template
@@ -429,9 +432,11 @@ production_manual:
only:
refs:
- master
- kubernetes: active
variables:
- $INCREMENTAL_ROLLOUT_MODE == "timed"
+ except:
+ variables:
+ - $KUBECONFIG == null
timed rollout 10%:
<<: *timed_rollout_template
@@ -525,11 +530,7 @@ rollout 100%:
}
function license_management() {
- # Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable"
- LICENSE_MANAGEMENT_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
-
- docker run --volume "$PWD:/code" \
- "registry.gitlab.com/gitlab-org/security-products/license-management:$LICENSE_MANAGEMENT_VERSION" analyze /code
+ /run.sh analyze .
}
function sast() {
@@ -823,7 +824,7 @@ rollout 100%:
function initialize_tiller() {
echo "Checking Tiller..."
- export HELM_HOST=":44134"
+ export HELM_HOST="localhost:44134"
tiller -listen ${HELM_HOST} -alsologtostderr > /dev/null 2>&1 &
echo "Tiller is listening on ${HELM_HOST}"
diff --git a/lib/gitlab/ci/templates/Maven.gitlab-ci.yml b/lib/gitlab/ci/templates/Maven.gitlab-ci.yml
index d61ff239e13..492b3d03db2 100644
--- a/lib/gitlab/ci/templates/Maven.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Maven.gitlab-ci.yml
@@ -15,7 +15,7 @@
# * Publishes the documentation for `master` branch.
variables:
- # This will supress any download for dependencies and plugins or upload messages which would clutter the console log.
+ # This will suppress any download for dependencies and plugins or upload messages which would clutter the console log.
# `showDateTime` will show the passed time in milliseconds. You need to specify `--batch-mode` to make this work.
MAVEN_OPTS: "-Dhttps.protocols=TLSv1.2 -Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN -Dorg.slf4j.simpleLogger.showDateTime=true -Djava.awt.headless=true"
# As of Maven 3.3.0 instead of this you may define these options in `.mvn/maven.config` so the same config is used
diff --git a/lib/gitlab/ci/trace.rb b/lib/gitlab/ci/trace.rb
index 93e219a21f9..8eccd262db9 100644
--- a/lib/gitlab/ci/trace.rb
+++ b/lib/gitlab/ci/trace.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Trace
diff --git a/lib/gitlab/ci/trace/chunked_io.rb b/lib/gitlab/ci/trace/chunked_io.rb
index 2147f62a84a..e9b3199d56e 100644
--- a/lib/gitlab/ci/trace/chunked_io.rb
+++ b/lib/gitlab/ci/trace/chunked_io.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
##
# This class is compatible with IO class (https://ruby-doc.org/core-2.3.1/IO.html)
# source: https://gitlab.com/snippets/1685610
@@ -66,8 +68,8 @@ module Gitlab
end
end
- def read(length = nil, outbuf = "")
- out = ""
+ def read(length = nil, outbuf = nil)
+ out = []
length ||= size - tell
@@ -83,17 +85,18 @@ module Gitlab
length -= chunk_data.bytesize
end
+ out = out.join
+
# If outbuf is passed, we put the output into the buffer. This supports IO.copy_stream functionality
if outbuf
- outbuf.slice!(0, outbuf.bytesize)
- outbuf << out
+ outbuf.replace(out)
end
out
end
def readline
- out = ""
+ out = []
until eof?
data = chunk_slice_from_offset
@@ -109,7 +112,7 @@ module Gitlab
end
end
- out
+ out.join
end
def write(data)
diff --git a/lib/gitlab/ci/trace/section_parser.rb b/lib/gitlab/ci/trace/section_parser.rb
index c09089d6475..f33f8cc56c1 100644
--- a/lib/gitlab/ci/trace/section_parser.rb
+++ b/lib/gitlab/ci/trace/section_parser.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Trace
diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb
index a71040e5e56..bd40fdf59b1 100644
--- a/lib/gitlab/ci/trace/stream.rb
+++ b/lib/gitlab/ci/trace/stream.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Trace
@@ -129,8 +131,7 @@ module Gitlab
debris = ''
until (buf = read_backward(BUFFER_SIZE)).empty?
- buf += debris
- debris, *lines = buf.each_line.to_a
+ debris, *lines = (buf + debris).each_line.to_a
lines.reverse_each do |line|
yield(line.force_encoding(Encoding.default_external))
end
diff --git a/lib/gitlab/ci/variables/collection.rb b/lib/gitlab/ci/variables/collection.rb
index ad30b3f427c..a7b4e0348c2 100644
--- a/lib/gitlab/ci/variables/collection.rb
+++ b/lib/gitlab/ci/variables/collection.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Variables
diff --git a/lib/gitlab/ci/variables/collection/item.rb b/lib/gitlab/ci/variables/collection/item.rb
index 7da6d09d440..fdf852e8788 100644
--- a/lib/gitlab/ci/variables/collection/item.rb
+++ b/lib/gitlab/ci/variables/collection/item.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Variables
diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb
index a427aa30683..e6ec400e476 100644
--- a/lib/gitlab/ci/yaml_processor.rb
+++ b/lib/gitlab/ci/yaml_processor.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class YamlProcessor
@@ -50,6 +52,8 @@ module Gitlab
after_script: job[:after_script],
environment: job[:environment],
retry: job[:retry],
+ parallel: job[:parallel],
+ instance: job[:instance],
start_in: job[:start_in]
}.compact }
end
@@ -102,7 +106,7 @@ module Gitlab
##
# Jobs
#
- @jobs = @ci_config.jobs
+ @jobs = Ci::Config::Normalizer.new(@ci_config.jobs).normalize_jobs
@jobs.each do |name, job|
# logical validation for job
diff --git a/lib/gitlab/cluster/puma_worker_killer_initializer.rb b/lib/gitlab/cluster/puma_worker_killer_initializer.rb
index 331c39f7d6b..4ed9a9a02ab 100644
--- a/lib/gitlab/cluster/puma_worker_killer_initializer.rb
+++ b/lib/gitlab/cluster/puma_worker_killer_initializer.rb
@@ -11,7 +11,11 @@ module Gitlab
# Importantly RAM is for _all_workers (ie, the cluster),
# not each worker as is the case with GITLAB_UNICORN_MEMORY_MAX
worker_count = puma_options[:workers] || 1
- config.ram = worker_count * puma_per_worker_max_memory_mb
+ # The Puma Worker Killer checks the total RAM used by both the master
+ # and worker processes. Bump the limits to N+1 instead of N workers
+ # to account for this:
+ # https://github.com/schneems/puma_worker_killer/blob/v0.1.0/lib/puma_worker_killer/puma_memory.rb#L57
+ config.ram = (worker_count + 1) * puma_per_worker_max_memory_mb
config.frequency = 20 # seconds
diff --git a/lib/gitlab/conflict/file.rb b/lib/gitlab/conflict/file.rb
index 30911b49b18..0ca99506311 100644
--- a/lib/gitlab/conflict/file.rb
+++ b/lib/gitlab/conflict/file.rb
@@ -1,8 +1,11 @@
+# frozen_string_literal: true
+
module Gitlab
module Conflict
class File
include Gitlab::Routing
include IconsHelper
+ include Gitlab::Utils::StrongMemoize
CONTEXT_LINES = 3
@@ -30,11 +33,8 @@ module Gitlab
end
def highlight_lines!
- their_file = lines.reject { |line| line.type == 'new' }.map(&:text).join("\n")
- our_file = lines.reject { |line| line.type == 'old' }.map(&:text).join("\n")
-
- their_highlight = Gitlab::Highlight.highlight(their_path, their_file, repository: repository).lines
- our_highlight = Gitlab::Highlight.highlight(our_path, our_file, repository: repository).lines
+ their_highlight = Gitlab::Highlight.highlight(their_path, their_lines, language: their_language).lines
+ our_highlight = Gitlab::Highlight.highlight(our_path, our_lines, language: our_language).lines
lines.each do |line|
line.rich_text =
@@ -182,6 +182,34 @@ module Gitlab
raw_line[:line_new], parent_file: self)
end
end
+
+ def their_language
+ strong_memoize(:their_language) do
+ repository.gitattribute(their_path, 'gitlab-language')
+ end
+ end
+
+ def our_language
+ strong_memoize(:our_language) do
+ if our_path == their_path
+ their_language
+ else
+ repository.gitattribute(our_path, 'gitlab-language')
+ end
+ end
+ end
+
+ def their_lines
+ strong_memoize(:their_lines) do
+ lines.reject { |line| line.type == 'new' }.map(&:text).join("\n")
+ end
+ end
+
+ def our_lines
+ strong_memoize(:our_lines) do
+ lines.reject { |line| line.type == 'old' }.map(&:text).join("\n")
+ end
+ end
end
end
end
diff --git a/lib/gitlab/conflict/file_collection.rb b/lib/gitlab/conflict/file_collection.rb
index 65a65b67975..53406af2c4e 100644
--- a/lib/gitlab/conflict/file_collection.rb
+++ b/lib/gitlab/conflict/file_collection.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Conflict
class FileCollection
diff --git a/lib/gitlab/cross_project_access/check_collection.rb b/lib/gitlab/cross_project_access/check_collection.rb
index 88376232065..55527ba5e87 100644
--- a/lib/gitlab/cross_project_access/check_collection.rb
+++ b/lib/gitlab/cross_project_access/check_collection.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
class CrossProjectAccess
class CheckCollection
diff --git a/lib/gitlab/cross_project_access/check_info.rb b/lib/gitlab/cross_project_access/check_info.rb
index e8a845c7f1e..2a9eacad680 100644
--- a/lib/gitlab/cross_project_access/check_info.rb
+++ b/lib/gitlab/cross_project_access/check_info.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
class CrossProjectAccess
class CheckInfo
diff --git a/lib/gitlab/cross_project_access/class_methods.rb b/lib/gitlab/cross_project_access/class_methods.rb
index 90eac94800c..64ad30794d3 100644
--- a/lib/gitlab/cross_project_access/class_methods.rb
+++ b/lib/gitlab/cross_project_access/class_methods.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
class CrossProjectAccess
module ClassMethods
diff --git a/lib/gitlab/cycle_analytics/base_event_fetcher.rb b/lib/gitlab/cycle_analytics/base_event_fetcher.rb
index e3e3767cc75..304d60996a6 100644
--- a/lib/gitlab/cycle_analytics/base_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/base_event_fetcher.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module CycleAnalytics
class BaseEventFetcher
diff --git a/lib/gitlab/cycle_analytics/base_query.rb b/lib/gitlab/cycle_analytics/base_query.rb
index 86d708be0d6..36231b187cd 100644
--- a/lib/gitlab/cycle_analytics/base_query.rb
+++ b/lib/gitlab/cycle_analytics/base_query.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module CycleAnalytics
module BaseQuery
diff --git a/lib/gitlab/cycle_analytics/base_stage.rb b/lib/gitlab/cycle_analytics/base_stage.rb
index 038d5a19bc4..e2d6a301734 100644
--- a/lib/gitlab/cycle_analytics/base_stage.rb
+++ b/lib/gitlab/cycle_analytics/base_stage.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module CycleAnalytics
class BaseStage
diff --git a/lib/gitlab/cycle_analytics/code_event_fetcher.rb b/lib/gitlab/cycle_analytics/code_event_fetcher.rb
index 06357c9b377..591db3c35e6 100644
--- a/lib/gitlab/cycle_analytics/code_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/code_event_fetcher.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module CycleAnalytics
class CodeEventFetcher < BaseEventFetcher
diff --git a/lib/gitlab/cycle_analytics/code_stage.rb b/lib/gitlab/cycle_analytics/code_stage.rb
index 5f9dc9a4303..2e5f9ef5a40 100644
--- a/lib/gitlab/cycle_analytics/code_stage.rb
+++ b/lib/gitlab/cycle_analytics/code_stage.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module CycleAnalytics
class CodeStage < BaseStage
diff --git a/lib/gitlab/cycle_analytics/event_fetcher.rb b/lib/gitlab/cycle_analytics/event_fetcher.rb
index 50e126cf00b..98a30a8fc97 100644
--- a/lib/gitlab/cycle_analytics/event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/event_fetcher.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module CycleAnalytics
module EventFetcher
diff --git a/lib/gitlab/cycle_analytics/issue_event_fetcher.rb b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb
index 1754f91dccb..30c6ead8968 100644
--- a/lib/gitlab/cycle_analytics/issue_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module CycleAnalytics
class IssueEventFetcher < BaseEventFetcher
diff --git a/lib/gitlab/cycle_analytics/issue_stage.rb b/lib/gitlab/cycle_analytics/issue_stage.rb
index 7b03811efb2..4eae2da512c 100644
--- a/lib/gitlab/cycle_analytics/issue_stage.rb
+++ b/lib/gitlab/cycle_analytics/issue_stage.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module CycleAnalytics
class IssueStage < BaseStage
diff --git a/lib/gitlab/cycle_analytics/metrics_tables.rb b/lib/gitlab/cycle_analytics/metrics_tables.rb
index f5d08c0b658..3e0302d308d 100644
--- a/lib/gitlab/cycle_analytics/metrics_tables.rb
+++ b/lib/gitlab/cycle_analytics/metrics_tables.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module CycleAnalytics
module MetricsTables
diff --git a/lib/gitlab/cycle_analytics/permissions.rb b/lib/gitlab/cycle_analytics/permissions.rb
index 1e11e84a9cb..afefd09b614 100644
--- a/lib/gitlab/cycle_analytics/permissions.rb
+++ b/lib/gitlab/cycle_analytics/permissions.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module CycleAnalytics
class Permissions
diff --git a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
index 086203b9ccc..db8ac3becea 100644
--- a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module CycleAnalytics
class PlanEventFetcher < BaseEventFetcher
diff --git a/lib/gitlab/cycle_analytics/plan_stage.rb b/lib/gitlab/cycle_analytics/plan_stage.rb
index 1a0afb56b4f..513e4575be0 100644
--- a/lib/gitlab/cycle_analytics/plan_stage.rb
+++ b/lib/gitlab/cycle_analytics/plan_stage.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module CycleAnalytics
class PlanStage < BaseStage
diff --git a/lib/gitlab/cycle_analytics/production_event_fetcher.rb b/lib/gitlab/cycle_analytics/production_event_fetcher.rb
index 0fa2e87f673..6681cb42c90 100644
--- a/lib/gitlab/cycle_analytics/production_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/production_event_fetcher.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module CycleAnalytics
class ProductionEventFetcher < IssueEventFetcher
diff --git a/lib/gitlab/cycle_analytics/production_helper.rb b/lib/gitlab/cycle_analytics/production_helper.rb
index d0ca62e46e4..aff65b150fb 100644
--- a/lib/gitlab/cycle_analytics/production_helper.rb
+++ b/lib/gitlab/cycle_analytics/production_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module CycleAnalytics
module ProductionHelper
diff --git a/lib/gitlab/cycle_analytics/production_stage.rb b/lib/gitlab/cycle_analytics/production_stage.rb
index 0fa8a65cb99..6fd7214dce7 100644
--- a/lib/gitlab/cycle_analytics/production_stage.rb
+++ b/lib/gitlab/cycle_analytics/production_stage.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module CycleAnalytics
class ProductionStage < BaseStage
diff --git a/lib/gitlab/cycle_analytics/review_event_fetcher.rb b/lib/gitlab/cycle_analytics/review_event_fetcher.rb
index dada819a2a8..de100295281 100644
--- a/lib/gitlab/cycle_analytics/review_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/review_event_fetcher.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module CycleAnalytics
class ReviewEventFetcher < BaseEventFetcher
diff --git a/lib/gitlab/cycle_analytics/review_stage.rb b/lib/gitlab/cycle_analytics/review_stage.rb
index cfbbdc43fd9..294b656bc55 100644
--- a/lib/gitlab/cycle_analytics/review_stage.rb
+++ b/lib/gitlab/cycle_analytics/review_stage.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module CycleAnalytics
class ReviewStage < BaseStage
diff --git a/lib/gitlab/cycle_analytics/stage.rb b/lib/gitlab/cycle_analytics/stage.rb
index 28e0455df59..1bd40a7aa18 100644
--- a/lib/gitlab/cycle_analytics/stage.rb
+++ b/lib/gitlab/cycle_analytics/stage.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module CycleAnalytics
module Stage
diff --git a/lib/gitlab/cycle_analytics/stage_summary.rb b/lib/gitlab/cycle_analytics/stage_summary.rb
index fc77bd86097..5198dd5b4eb 100644
--- a/lib/gitlab/cycle_analytics/stage_summary.rb
+++ b/lib/gitlab/cycle_analytics/stage_summary.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module CycleAnalytics
class StageSummary
diff --git a/lib/gitlab/cycle_analytics/staging_event_fetcher.rb b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb
index 2f014153ca5..70ce82383b3 100644
--- a/lib/gitlab/cycle_analytics/staging_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module CycleAnalytics
class StagingEventFetcher < BaseEventFetcher
diff --git a/lib/gitlab/cycle_analytics/staging_stage.rb b/lib/gitlab/cycle_analytics/staging_stage.rb
index d5684bb9201..dbc2414ff66 100644
--- a/lib/gitlab/cycle_analytics/staging_stage.rb
+++ b/lib/gitlab/cycle_analytics/staging_stage.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module CycleAnalytics
class StagingStage < BaseStage
diff --git a/lib/gitlab/cycle_analytics/summary/base.rb b/lib/gitlab/cycle_analytics/summary/base.rb
index a917ddccac7..709221c648e 100644
--- a/lib/gitlab/cycle_analytics/summary/base.rb
+++ b/lib/gitlab/cycle_analytics/summary/base.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module CycleAnalytics
module Summary
diff --git a/lib/gitlab/cycle_analytics/summary/commit.rb b/lib/gitlab/cycle_analytics/summary/commit.rb
index 550c1755a71..f0019b26fa2 100644
--- a/lib/gitlab/cycle_analytics/summary/commit.rb
+++ b/lib/gitlab/cycle_analytics/summary/commit.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module CycleAnalytics
module Summary
diff --git a/lib/gitlab/cycle_analytics/summary/deploy.rb b/lib/gitlab/cycle_analytics/summary/deploy.rb
index 099d798aac6..3b56dc2a7bc 100644
--- a/lib/gitlab/cycle_analytics/summary/deploy.rb
+++ b/lib/gitlab/cycle_analytics/summary/deploy.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module CycleAnalytics
module Summary
diff --git a/lib/gitlab/cycle_analytics/summary/issue.rb b/lib/gitlab/cycle_analytics/summary/issue.rb
index 9bbf7a2685f..51695c86192 100644
--- a/lib/gitlab/cycle_analytics/summary/issue.rb
+++ b/lib/gitlab/cycle_analytics/summary/issue.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module CycleAnalytics
module Summary
diff --git a/lib/gitlab/cycle_analytics/test_event_fetcher.rb b/lib/gitlab/cycle_analytics/test_event_fetcher.rb
index a2589c6601a..4d5ea5b7c34 100644
--- a/lib/gitlab/cycle_analytics/test_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/test_event_fetcher.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module CycleAnalytics
class TestEventFetcher < StagingEventFetcher
diff --git a/lib/gitlab/cycle_analytics/test_stage.rb b/lib/gitlab/cycle_analytics/test_stage.rb
index 0e9d235ca79..c31b664148b 100644
--- a/lib/gitlab/cycle_analytics/test_stage.rb
+++ b/lib/gitlab/cycle_analytics/test_stage.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module CycleAnalytics
class TestStage < BaseStage
diff --git a/lib/gitlab/cycle_analytics/updater.rb b/lib/gitlab/cycle_analytics/updater.rb
index 953268ebd46..c642809a792 100644
--- a/lib/gitlab/cycle_analytics/updater.rb
+++ b/lib/gitlab/cycle_analytics/updater.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module CycleAnalytics
class Updater
diff --git a/lib/gitlab/cycle_analytics/usage_data.rb b/lib/gitlab/cycle_analytics/usage_data.rb
index 5122e3417ca..913ee373f54 100644
--- a/lib/gitlab/cycle_analytics/usage_data.rb
+++ b/lib/gitlab/cycle_analytics/usage_data.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module CycleAnalytics
class UsageData
diff --git a/lib/gitlab/data_builder/build.rb b/lib/gitlab/data_builder/build.rb
index 0b71b31a476..3407380127e 100644
--- a/lib/gitlab/data_builder/build.rb
+++ b/lib/gitlab/data_builder/build.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module DataBuilder
module Build
diff --git a/lib/gitlab/data_builder/note.rb b/lib/gitlab/data_builder/note.rb
index f573368e572..65601dcdf31 100644
--- a/lib/gitlab/data_builder/note.rb
+++ b/lib/gitlab/data_builder/note.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module DataBuilder
module Note
diff --git a/lib/gitlab/data_builder/pipeline.rb b/lib/gitlab/data_builder/pipeline.rb
index f382992cb0a..76c8b4ec5c2 100644
--- a/lib/gitlab/data_builder/pipeline.rb
+++ b/lib/gitlab/data_builder/pipeline.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module DataBuilder
module Pipeline
diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb
index b498f113859..9bf2f9291a8 100644
--- a/lib/gitlab/data_builder/push.rb
+++ b/lib/gitlab/data_builder/push.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module DataBuilder
module Push
diff --git a/lib/gitlab/data_builder/repository.rb b/lib/gitlab/data_builder/repository.rb
index c9c13ec6487..0e627fd623e 100644
--- a/lib/gitlab/data_builder/repository.rb
+++ b/lib/gitlab/data_builder/repository.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module DataBuilder
module Repository
diff --git a/lib/gitlab/data_builder/wiki_page.rb b/lib/gitlab/data_builder/wiki_page.rb
index 226974b698c..9368446fa59 100644
--- a/lib/gitlab/data_builder/wiki_page.rb
+++ b/lib/gitlab/data_builder/wiki_page.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module DataBuilder
module WikiPage
diff --git a/lib/gitlab/database/arel_methods.rb b/lib/gitlab/database/arel_methods.rb
index d7e3ce08b32..991e4152dcb 100644
--- a/lib/gitlab/database/arel_methods.rb
+++ b/lib/gitlab/database/arel_methods.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Database
module ArelMethods
diff --git a/lib/gitlab/database/count.rb b/lib/gitlab/database/count.rb
index 5f549ed2b3c..ea6529e2dc4 100644
--- a/lib/gitlab/database/count.rb
+++ b/lib/gitlab/database/count.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# For large tables, PostgreSQL can take a long time to count rows due to MVCC.
# We can optimize this by using the reltuples count as described in https://wiki.postgresql.org/wiki/Slow_Counting.
module Gitlab
diff --git a/lib/gitlab/database/date_time.rb b/lib/gitlab/database/date_time.rb
index 25e56998038..79d2caff151 100644
--- a/lib/gitlab/database/date_time.rb
+++ b/lib/gitlab/database/date_time.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Database
module DateTime
diff --git a/lib/gitlab/database/grant.rb b/lib/gitlab/database/grant.rb
index 7d334a79009..862ab96c887 100644
--- a/lib/gitlab/database/grant.rb
+++ b/lib/gitlab/database/grant.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Database
# Model that can be used for querying permissions of a SQL user.
diff --git a/lib/gitlab/database/median.rb b/lib/gitlab/database/median.rb
index f64e3d53138..0da5119a3ed 100644
--- a/lib/gitlab/database/median.rb
+++ b/lib/gitlab/database/median.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# https://www.periscopedata.com/blog/medians-in-sql.html
module Gitlab
module Database
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index a17f27a3147..134d1e7a724 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Database
module MigrationHelpers
@@ -879,7 +881,7 @@ module Gitlab
columns(table).find { |column| column.name == name }
end
- # This will replace the first occurance of a string in a column with
+ # This will replace the first occurrence of a string in a column with
# the replacement
# On postgresql we can use `regexp_replace` for that.
# On mysql we find the location of the pattern, and overwrite it
diff --git a/lib/gitlab/database/multi_threaded_migration.rb b/lib/gitlab/database/multi_threaded_migration.rb
index 7ae5a4c17c8..1d39a3d0b57 100644
--- a/lib/gitlab/database/multi_threaded_migration.rb
+++ b/lib/gitlab/database/multi_threaded_migration.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Database
module MultiThreadedMigration
diff --git a/lib/gitlab/database/read_only_relation.rb b/lib/gitlab/database/read_only_relation.rb
index 4571ad122ce..2362208e5dd 100644
--- a/lib/gitlab/database/read_only_relation.rb
+++ b/lib/gitlab/database/read_only_relation.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Database
# Module that can be injected into a ActiveRecord::Relation to make it
diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1.rb
index f333ff22300..2314246da55 100644
--- a/lib/gitlab/database/rename_reserved_paths_migration/v1.rb
+++ b/lib/gitlab/database/rename_reserved_paths_migration/v1.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# This module can be included in migrations to make it easier to rename paths
# of `Namespace` & `Project` models certain paths would become `reserved`.
#
diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb
index 26ae6966746..f1dc3ed74fe 100644
--- a/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb
+++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Database
module RenameReservedPathsMigration
diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb
index 14de28a1d08..a5b42bbfdd9 100644
--- a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb
+++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Database
module RenameReservedPathsMigration
diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb
index 73971af6a74..6bbad707f0f 100644
--- a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb
+++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Database
module RenameReservedPathsMigration
diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb
index 827aeb12a02..580be9fe267 100644
--- a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb
+++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Database
module RenameReservedPathsMigration
diff --git a/lib/gitlab/database/sha_attribute.rb b/lib/gitlab/database/sha_attribute.rb
index b2d8ee81977..6516d6e648d 100644
--- a/lib/gitlab/database/sha_attribute.rb
+++ b/lib/gitlab/database/sha_attribute.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Database
BINARY_TYPE =
diff --git a/lib/gitlab/dependency_linker/base_linker.rb b/lib/gitlab/dependency_linker/base_linker.rb
index d2360583741..ac2efe598b4 100644
--- a/lib/gitlab/dependency_linker/base_linker.rb
+++ b/lib/gitlab/dependency_linker/base_linker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module DependencyLinker
class BaseLinker
diff --git a/lib/gitlab/dependency_linker/cartfile_linker.rb b/lib/gitlab/dependency_linker/cartfile_linker.rb
index 4f69f2c4ab2..0e33f0956dd 100644
--- a/lib/gitlab/dependency_linker/cartfile_linker.rb
+++ b/lib/gitlab/dependency_linker/cartfile_linker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module DependencyLinker
class CartfileLinker < MethodLinker
diff --git a/lib/gitlab/dependency_linker/cocoapods.rb b/lib/gitlab/dependency_linker/cocoapods.rb
index 2fbde7da1b4..38eabe303de 100644
--- a/lib/gitlab/dependency_linker/cocoapods.rb
+++ b/lib/gitlab/dependency_linker/cocoapods.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module DependencyLinker
module Cocoapods
diff --git a/lib/gitlab/dependency_linker/composer_json_linker.rb b/lib/gitlab/dependency_linker/composer_json_linker.rb
index cfd4ec15125..22d2bead891 100644
--- a/lib/gitlab/dependency_linker/composer_json_linker.rb
+++ b/lib/gitlab/dependency_linker/composer_json_linker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module DependencyLinker
class ComposerJsonLinker < PackageJsonLinker
diff --git a/lib/gitlab/dependency_linker/gemfile_linker.rb b/lib/gitlab/dependency_linker/gemfile_linker.rb
index bfea836bcb2..8ab219c4962 100644
--- a/lib/gitlab/dependency_linker/gemfile_linker.rb
+++ b/lib/gitlab/dependency_linker/gemfile_linker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module DependencyLinker
class GemfileLinker < MethodLinker
diff --git a/lib/gitlab/dependency_linker/gemspec_linker.rb b/lib/gitlab/dependency_linker/gemspec_linker.rb
index f1783ee2ab4..b924ea86d89 100644
--- a/lib/gitlab/dependency_linker/gemspec_linker.rb
+++ b/lib/gitlab/dependency_linker/gemspec_linker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module DependencyLinker
class GemspecLinker < MethodLinker
diff --git a/lib/gitlab/dependency_linker/godeps_json_linker.rb b/lib/gitlab/dependency_linker/godeps_json_linker.rb
index fe091baee6d..d24c137793e 100644
--- a/lib/gitlab/dependency_linker/godeps_json_linker.rb
+++ b/lib/gitlab/dependency_linker/godeps_json_linker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module DependencyLinker
class GodepsJsonLinker < JsonLinker
diff --git a/lib/gitlab/dependency_linker/json_linker.rb b/lib/gitlab/dependency_linker/json_linker.rb
index a8ef25233d8..298d214df61 100644
--- a/lib/gitlab/dependency_linker/json_linker.rb
+++ b/lib/gitlab/dependency_linker/json_linker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module DependencyLinker
class JsonLinker < BaseLinker
diff --git a/lib/gitlab/dependency_linker/method_linker.rb b/lib/gitlab/dependency_linker/method_linker.rb
index 0ffa2a83c93..d4d85bb3390 100644
--- a/lib/gitlab/dependency_linker/method_linker.rb
+++ b/lib/gitlab/dependency_linker/method_linker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module DependencyLinker
class MethodLinker < BaseLinker
diff --git a/lib/gitlab/dependency_linker/package_json_linker.rb b/lib/gitlab/dependency_linker/package_json_linker.rb
index 330c95f0880..578e25f806a 100644
--- a/lib/gitlab/dependency_linker/package_json_linker.rb
+++ b/lib/gitlab/dependency_linker/package_json_linker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module DependencyLinker
class PackageJsonLinker < JsonLinker
diff --git a/lib/gitlab/dependency_linker/podfile_linker.rb b/lib/gitlab/dependency_linker/podfile_linker.rb
index 60ad166ea17..def9b04cca9 100644
--- a/lib/gitlab/dependency_linker/podfile_linker.rb
+++ b/lib/gitlab/dependency_linker/podfile_linker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module DependencyLinker
class PodfileLinker < GemfileLinker
diff --git a/lib/gitlab/dependency_linker/podspec_json_linker.rb b/lib/gitlab/dependency_linker/podspec_json_linker.rb
index d82237ed3f1..1a2493e7cc0 100644
--- a/lib/gitlab/dependency_linker/podspec_json_linker.rb
+++ b/lib/gitlab/dependency_linker/podspec_json_linker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module DependencyLinker
class PodspecJsonLinker < JsonLinker
diff --git a/lib/gitlab/dependency_linker/podspec_linker.rb b/lib/gitlab/dependency_linker/podspec_linker.rb
index 924e55e9820..6b1758c5a43 100644
--- a/lib/gitlab/dependency_linker/podspec_linker.rb
+++ b/lib/gitlab/dependency_linker/podspec_linker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module DependencyLinker
class PodspecLinker < MethodLinker
diff --git a/lib/gitlab/dependency_linker/requirements_txt_linker.rb b/lib/gitlab/dependency_linker/requirements_txt_linker.rb
index 9c9620bc36a..f630c13b760 100644
--- a/lib/gitlab/dependency_linker/requirements_txt_linker.rb
+++ b/lib/gitlab/dependency_linker/requirements_txt_linker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module DependencyLinker
class RequirementsTxtLinker < BaseLinker
diff --git a/lib/gitlab/diff/diff_refs.rb b/lib/gitlab/diff/diff_refs.rb
index 81df47964be..d4823f60826 100644
--- a/lib/gitlab/diff/diff_refs.rb
+++ b/lib/gitlab/diff/diff_refs.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Diff
class DiffRefs
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index fb117baca9e..8ba44dff06f 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Diff
class File
@@ -26,6 +28,7 @@ module Gitlab
@repository = repository
@diff_refs = diff_refs
@fallback_diff_refs = fallback_diff_refs
+ @unfolded = false
# Ensure items are collected in the the batch
new_blob_lazy
@@ -135,6 +138,24 @@ module Gitlab
Gitlab::Diff::Parser.new.parse(raw_diff.each_line, diff_file: self).to_a
end
+ # Changes diff_lines according to the given position. That is,
+ # it checks whether the position requires blob lines into the diff
+ # in order to be presented.
+ def unfold_diff_lines(position)
+ return unless position
+
+ unfolder = Gitlab::Diff::LinesUnfolder.new(self, position)
+
+ if unfolder.unfold_required?
+ @diff_lines = unfolder.unfolded_diff_lines
+ @unfolded = true
+ end
+ end
+
+ def unfolded?
+ @unfolded
+ end
+
def highlighted_diff_lines
@highlighted_diff_lines ||=
Gitlab::Diff::Highlight.new(self, repository: self.repository).highlight
diff --git a/lib/gitlab/diff/file_collection/base.rb b/lib/gitlab/diff/file_collection/base.rb
index b79ff771a2b..10df037a0dd 100644
--- a/lib/gitlab/diff/file_collection/base.rb
+++ b/lib/gitlab/diff/file_collection/base.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Diff
module FileCollection
@@ -17,7 +19,6 @@ module Gitlab
@diffable = diffable
@include_stats = diff_options.delete(:include_stats)
- @diffs = diffable.raw_diffs(diff_options)
@project = project
@diff_options = diff_options
@diff_refs = diff_refs
@@ -25,8 +26,12 @@ module Gitlab
@repository = project.repository
end
+ def diffs
+ @diffs ||= diffable.raw_diffs(diff_options)
+ end
+
def diff_files
- @diff_files ||= @diffs.decorate! { |diff| decorate_diff!(diff) }
+ @diff_files ||= diffs.decorate! { |diff| decorate_diff!(diff) }
end
def diff_file_with_old_path(old_path)
diff --git a/lib/gitlab/diff/file_collection/commit.rb b/lib/gitlab/diff/file_collection/commit.rb
index 4dc297ec036..7b1d6171e82 100644
--- a/lib/gitlab/diff/file_collection/commit.rb
+++ b/lib/gitlab/diff/file_collection/commit.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Diff
module FileCollection
diff --git a/lib/gitlab/diff/file_collection/compare.rb b/lib/gitlab/diff/file_collection/compare.rb
index 20d8f891cc3..586c5cf87af 100644
--- a/lib/gitlab/diff/file_collection/compare.rb
+++ b/lib/gitlab/diff/file_collection/compare.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Diff
module FileCollection
diff --git a/lib/gitlab/diff/file_collection/merge_request_diff.rb b/lib/gitlab/diff/file_collection/merge_request_diff.rb
index 0dd073a3a8e..e29bf75f341 100644
--- a/lib/gitlab/diff/file_collection/merge_request_diff.rb
+++ b/lib/gitlab/diff/file_collection/merge_request_diff.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Diff
module FileCollection
diff --git a/lib/gitlab/diff/formatters/base_formatter.rb b/lib/gitlab/diff/formatters/base_formatter.rb
index 5e923b9e602..9704aed82c1 100644
--- a/lib/gitlab/diff/formatters/base_formatter.rb
+++ b/lib/gitlab/diff/formatters/base_formatter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Diff
module Formatters
diff --git a/lib/gitlab/diff/formatters/image_formatter.rb b/lib/gitlab/diff/formatters/image_formatter.rb
index ccd0d309972..5bc9f0c337f 100644
--- a/lib/gitlab/diff/formatters/image_formatter.rb
+++ b/lib/gitlab/diff/formatters/image_formatter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Diff
module Formatters
diff --git a/lib/gitlab/diff/formatters/text_formatter.rb b/lib/gitlab/diff/formatters/text_formatter.rb
index 01c7e9f51ab..f6e247ef665 100644
--- a/lib/gitlab/diff/formatters/text_formatter.rb
+++ b/lib/gitlab/diff/formatters/text_formatter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Diff
module Formatters
diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb
index a605ddb5c33..d2484217ab9 100644
--- a/lib/gitlab/diff/highlight.rb
+++ b/lib/gitlab/diff/highlight.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Diff
class Highlight
@@ -79,7 +81,7 @@ module Gitlab
return [] unless blob
blob.load_all_data!
- Gitlab::Highlight.highlight(blob.path, blob.data, repository: repository).lines
+ blob.present.highlight.lines
end
end
end
diff --git a/lib/gitlab/diff/image_point.rb b/lib/gitlab/diff/image_point.rb
index 1f157354ea4..a3ce032f8e2 100644
--- a/lib/gitlab/diff/image_point.rb
+++ b/lib/gitlab/diff/image_point.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Diff
class ImagePoint
diff --git a/lib/gitlab/diff/inline_diff.rb b/lib/gitlab/diff/inline_diff.rb
index 72d5ec547da..5815d1bae4a 100644
--- a/lib/gitlab/diff/inline_diff.rb
+++ b/lib/gitlab/diff/inline_diff.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Diff
class InlineDiff
@@ -71,7 +73,7 @@ module Gitlab
def find_changed_line_pairs(lines)
# Prefixes of all diff lines, indicating their types
# For example: `" - + -+ ---+++ --+ -++"`
- line_prefixes = lines.each_with_object("") { |line, s| s << (line[0] || ' ') }.gsub(/[^ +-]/, ' ')
+ line_prefixes = lines.each_with_object(+"") { |line, s| s << (line[0] || ' ') }.gsub(/[^ +-]/, ' ')
changed_line_pairs = []
line_prefixes.scan(LINE_PAIRS_PATTERN) do
diff --git a/lib/gitlab/diff/inline_diff_markdown_marker.rb b/lib/gitlab/diff/inline_diff_markdown_marker.rb
index c2a2eb15931..3c536c43a9e 100644
--- a/lib/gitlab/diff/inline_diff_markdown_marker.rb
+++ b/lib/gitlab/diff/inline_diff_markdown_marker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Diff
class InlineDiffMarkdownMarker < Gitlab::StringRangeMarker
diff --git a/lib/gitlab/diff/inline_diff_marker.rb b/lib/gitlab/diff/inline_diff_marker.rb
index 81e91ea0ab7..1bbde1ffd2a 100644
--- a/lib/gitlab/diff/inline_diff_marker.rb
+++ b/lib/gitlab/diff/inline_diff_marker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Diff
class InlineDiffMarker < Gitlab::StringRangeMarker
diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb
index 5b67cd46c48..f0c4977fc50 100644
--- a/lib/gitlab/diff/line.rb
+++ b/lib/gitlab/diff/line.rb
@@ -1,11 +1,13 @@
+# frozen_string_literal: true
+
module Gitlab
module Diff
class Line
SERIALIZE_KEYS = %i(line_code rich_text text type index old_pos new_pos).freeze
- attr_reader :line_code, :type, :index, :old_pos, :new_pos
+ attr_reader :line_code, :type, :old_pos, :new_pos
attr_writer :rich_text
- attr_accessor :text
+ attr_accessor :text, :index
def initialize(text, type, index, old_pos, new_pos, parent_file: nil, line_code: nil, rich_text: nil)
@text, @type, @index = text, type, index
@@ -19,7 +21,14 @@ module Gitlab
end
def self.init_from_hash(hash)
- new(hash[:text], hash[:type], hash[:index], hash[:old_pos], hash[:new_pos], line_code: hash[:line_code], rich_text: hash[:rich_text])
+ new(hash[:text],
+ hash[:type],
+ hash[:index],
+ hash[:old_pos],
+ hash[:new_pos],
+ parent_file: hash[:parent_file],
+ line_code: hash[:line_code],
+ rich_text: hash[:rich_text])
end
def to_hash
diff --git a/lib/gitlab/diff/line_mapper.rb b/lib/gitlab/diff/line_mapper.rb
index cf71d47df8e..fba7bff4781 100644
--- a/lib/gitlab/diff/line_mapper.rb
+++ b/lib/gitlab/diff/line_mapper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# When provided a diff for a specific file, maps old line numbers to new line
# numbers and back, to find out where a specific line in a file was moved by the
# changes.
diff --git a/lib/gitlab/diff/lines_unfolder.rb b/lib/gitlab/diff/lines_unfolder.rb
new file mode 100644
index 00000000000..9306b7e16a2
--- /dev/null
+++ b/lib/gitlab/diff/lines_unfolder.rb
@@ -0,0 +1,235 @@
+# frozen_string_literal: true
+
+# Given a position, calculates which Blob lines should be extracted, treated and
+# injected in the current diff file lines in order to present a "unfolded" diff.
+module Gitlab
+ module Diff
+ class LinesUnfolder
+ include Gitlab::Utils::StrongMemoize
+
+ UNFOLD_CONTEXT_SIZE = 3
+
+ def initialize(diff_file, position)
+ @diff_file = diff_file
+ @blob = diff_file.old_blob
+ @position = position
+ @generate_top_match_line = true
+ @generate_bottom_match_line = true
+
+ # These methods update `@generate_top_match_line` and
+ # `@generate_bottom_match_line`.
+ @from_blob_line = calculate_from_blob_line!
+ @to_blob_line = calculate_to_blob_line!
+ end
+
+ # Returns merged diff lines with required blob lines with correct
+ # positions.
+ def unfolded_diff_lines
+ strong_memoize(:unfolded_diff_lines) do
+ next unless unfold_required?
+
+ merged_diff_with_blob_lines
+ end
+ end
+
+ # Returns the extracted lines from the old blob which should be merged
+ # with the current diff lines.
+ def blob_lines
+ strong_memoize(:blob_lines) do
+ # Blob lines, unlike diffs, doesn't start with an empty space for
+ # unchanged line, so the parsing and highlighting step can get fuzzy
+ # without the following change.
+ line_prefix = ' '
+ blob_as_diff_lines = @blob.data.each_line.map { |line| "#{line_prefix}#{line}" }
+
+ lines = Gitlab::Diff::Parser.new.parse(blob_as_diff_lines, diff_file: @diff_file).to_a
+
+ from = from_blob_line - 1
+ to = to_blob_line - 1
+
+ lines[from..to]
+ end
+ end
+
+ def unfold_required?
+ strong_memoize(:unfold_required) do
+ next false unless @diff_file.text?
+ next false unless @position.unchanged?
+ next false if @diff_file.new_file? || @diff_file.deleted_file?
+ next false unless @position.old_line
+ # Invalid position (MR import scenario)
+ next false if @position.old_line > @blob.lines.size
+ next false if @diff_file.diff_lines.empty?
+ next false if @diff_file.line_for_position(@position)
+ next false unless unfold_line
+
+ true
+ end
+ end
+
+ private
+
+ attr_reader :from_blob_line, :to_blob_line
+
+ def merged_diff_with_blob_lines
+ lines = @diff_file.diff_lines
+ match_line = unfold_line
+ insert_index = bottom? ? -1 : match_line.index
+
+ lines -= [match_line] unless bottom?
+
+ lines.insert(insert_index, *blob_lines_with_matches)
+
+ # The inserted blob lines have invalid indexes, so we need
+ # to reindex them.
+ reindex(lines)
+
+ lines
+ end
+
+ # Returns 'unchanged' blob lines with recalculated `old_pos` and
+ # `new_pos` and the recalculated new match line (needed if we for instance
+ # we unfolded once, but there are still folded lines).
+ def blob_lines_with_matches
+ old_pos = from_blob_line
+ new_pos = from_blob_line + offset
+
+ new_blob_lines = []
+
+ new_blob_lines.push(top_blob_match_line) if top_blob_match_line
+
+ blob_lines.each do |line|
+ new_blob_lines << Gitlab::Diff::Line.new(line.text, line.type, nil, old_pos, new_pos,
+ parent_file: @diff_file)
+
+ old_pos += 1
+ new_pos += 1
+ end
+
+ new_blob_lines.push(bottom_blob_match_line) if bottom_blob_match_line
+
+ new_blob_lines
+ end
+
+ def reindex(lines)
+ lines.each_with_index { |line, i| line.index = i }
+ end
+
+ def top_blob_match_line
+ strong_memoize(:top_blob_match_line) do
+ next unless @generate_top_match_line
+
+ old_pos = from_blob_line
+ new_pos = from_blob_line + offset
+
+ build_match_line(old_pos, new_pos)
+ end
+ end
+
+ def bottom_blob_match_line
+ strong_memoize(:bottom_blob_match_line) do
+ # The bottom line match addition is already handled on
+ # Diff::File#diff_lines_for_serializer
+ next if bottom?
+ next unless @generate_bottom_match_line
+
+ position = line_after_unfold_position.old_pos
+
+ old_pos = position
+ new_pos = position + offset
+
+ build_match_line(old_pos, new_pos)
+ end
+ end
+
+ def build_match_line(old_pos, new_pos)
+ blob_lines_length = blob_lines.length
+ old_line_ref = [old_pos, blob_lines_length].join(',')
+ new_line_ref = [new_pos, blob_lines_length].join(',')
+ new_match_line_str = "@@ -#{old_line_ref}+#{new_line_ref} @@"
+
+ Gitlab::Diff::Line.new(new_match_line_str, 'match', nil, old_pos, new_pos)
+ end
+
+ # Returns the first line position that should be extracted
+ # from `blob_lines`.
+ def calculate_from_blob_line!
+ return unless unfold_required?
+
+ from = comment_position - UNFOLD_CONTEXT_SIZE
+
+ # There's no line before the match if it's in the top-most
+ # position.
+ prev_line_number = line_before_unfold_position&.old_pos || 0
+
+ if from <= prev_line_number + 1
+ @generate_top_match_line = false
+ from = prev_line_number + 1
+ end
+
+ from
+ end
+
+ # Returns the last line position that should be extracted
+ # from `blob_lines`.
+ def calculate_to_blob_line!
+ return unless unfold_required?
+
+ to = comment_position + UNFOLD_CONTEXT_SIZE
+
+ return to if bottom?
+
+ next_line_number = line_after_unfold_position.old_pos
+
+ if to >= next_line_number - 1
+ @generate_bottom_match_line = false
+ to = next_line_number - 1
+ end
+
+ to
+ end
+
+ def offset
+ unfold_line.new_pos - unfold_line.old_pos
+ end
+
+ def line_before_unfold_position
+ return unless index = unfold_line&.index
+
+ @diff_file.diff_lines[index - 1] if index > 0
+ end
+
+ def line_after_unfold_position
+ return unless index = unfold_line&.index
+
+ @diff_file.diff_lines[index + 1] if index >= 0
+ end
+
+ def bottom?
+ strong_memoize(:bottom) do
+ @position.old_line > last_line.old_pos
+ end
+ end
+
+ # Returns the line which needed to be expanded in order to send a comment
+ # in `@position`.
+ def unfold_line
+ strong_memoize(:unfold_line) do
+ next last_line if bottom?
+
+ @diff_file.diff_lines.find do |line|
+ line.old_pos > comment_position && line.type == 'match'
+ end
+ end
+ end
+
+ def comment_position
+ @position.old_line
+ end
+
+ def last_line
+ @diff_file.diff_lines.last
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/parallel_diff.rb b/lib/gitlab/diff/parallel_diff.rb
index 0cb26fa45c8..77b65fea726 100644
--- a/lib/gitlab/diff/parallel_diff.rb
+++ b/lib/gitlab/diff/parallel_diff.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Diff
class ParallelDiff
diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb
index 7ae7ed286ed..4a47e4b80b6 100644
--- a/lib/gitlab/diff/parser.rb
+++ b/lib/gitlab/diff/parser.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Diff
class Parser
diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb
index f967494199e..e8f98f52111 100644
--- a/lib/gitlab/diff/position.rb
+++ b/lib/gitlab/diff/position.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Defines a specific location, identified by paths line numbers and image coordinates,
# within a specific diff, identified by start, head and base commit ids.
module Gitlab
@@ -101,6 +103,10 @@ module Gitlab
@diff_refs ||= DiffRefs.new(base_sha: base_sha, start_sha: start_sha, head_sha: head_sha)
end
+ def unfolded_diff?(repository)
+ diff_file(repository)&.unfolded?
+ end
+
def diff_file(repository)
return @diff_file if defined?(@diff_file)
@@ -134,7 +140,13 @@ module Gitlab
return unless diff_refs.complete?
return unless comparison = diff_refs.compare_in(repository.project)
- comparison.diffs(diff_options).diff_files.first
+ file = comparison.diffs(diff_options).diff_files.first
+
+ # We need to unfold diff lines according to the position in order
+ # to correctly calculate the line code and trace position changes.
+ file&.unfold_diff_lines(self)
+
+ file
end
def get_formatter_class(type)
diff --git a/lib/gitlab/diff/position_tracer.rb b/lib/gitlab/diff/position_tracer.rb
index b68a1636814..af3df820422 100644
--- a/lib/gitlab/diff/position_tracer.rb
+++ b/lib/gitlab/diff/position_tracer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Finds the diff position in the new diff that corresponds to the same location
# specified by the provided position in the old diff.
module Gitlab
@@ -24,7 +26,7 @@ module Gitlab
# head of `feature` was commit B, resulting in the original diff A->B.
# Since creation, `master` was updated to C.
# Now `feature` is being updated to D, and the newly generated MR diff is C->D.
- # It is possible that C and D are direct decendants of A and B respectively,
+ # It is possible that C and D are direct descendants of A and B respectively,
# but this isn't necessarily the case as rebases and merges come into play.
#
# Suppose we have a diff note on the original diff A->B. Now that the MR
diff --git a/lib/gitlab/downtime_check/message.rb b/lib/gitlab/downtime_check/message.rb
index 543e62794c5..ec38bd769a3 100644
--- a/lib/gitlab/downtime_check/message.rb
+++ b/lib/gitlab/downtime_check/message.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
class DowntimeCheck
class Message
@@ -18,13 +20,13 @@ module Gitlab
def to_s
label = offline ? OFFLINE : ONLINE
- message = "[#{label}]: #{path}"
+ message = ["[#{label}]: #{path}"]
if reason?
- message += ":\n\n#{reason}\n\n"
+ message << ":\n\n#{reason}\n\n"
end
- message
+ message.join
end
def reason?
diff --git a/lib/gitlab/email/attachment_uploader.rb b/lib/gitlab/email/attachment_uploader.rb
index 83440ae227d..a826519b2dd 100644
--- a/lib/gitlab/email/attachment_uploader.rb
+++ b/lib/gitlab/email/attachment_uploader.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Email
class AttachmentUploader
diff --git a/lib/gitlab/email/handler/create_merge_request_handler.rb b/lib/gitlab/email/handler/create_merge_request_handler.rb
index e68ae60ff98..5772727e855 100644
--- a/lib/gitlab/email/handler/create_merge_request_handler.rb
+++ b/lib/gitlab/email/handler/create_merge_request_handler.rb
@@ -44,10 +44,26 @@ module Gitlab
@project ||= Project.find_by_full_path(project_path)
end
+ def metrics_params
+ super.merge(includes_patches: patch_attachments.any?)
+ end
+
private
+ def build_merge_request
+ MergeRequests::BuildService.new(project, author, merge_request_params).execute
+ end
+
def create_merge_request
- merge_request = MergeRequests::BuildService.new(project, author, merge_request_params).execute
+ merge_request = build_merge_request
+
+ if patch_attachments.any?
+ apply_patches_to_source_branch(start_branch: merge_request.target_branch)
+ remove_patch_attachments
+ # Rebuild the merge request as the source branch might just have
+ # been created, so we should re-validate.
+ merge_request = build_merge_request
+ end
if merge_request.errors.any?
merge_request
@@ -59,12 +75,42 @@ module Gitlab
def merge_request_params
params = {
source_project_id: project.id,
- source_branch: mail.subject,
+ source_branch: source_branch,
target_project_id: project.id
}
params[:description] = message if message.present?
params
end
+
+ def apply_patches_to_source_branch(start_branch:)
+ patches = patch_attachments.map { |patch| patch.body.decoded }
+
+ result = Commits::CommitPatchService
+ .new(project, author, branch_name: source_branch, patches: patches, start_branch: start_branch)
+ .execute
+
+ if result[:status] != :success
+ message = "Could not apply patches to #{source_branch}:\n#{result[:message]}"
+ raise InvalidAttachment, message
+ end
+ end
+
+ def remove_patch_attachments
+ patch_attachments.each { |patch| mail.parts.delete(patch) }
+ # reset the message, so it needs to be reporocessed when the attachments
+ # have been modified
+ @message = nil
+ end
+
+ def patch_attachments
+ @patches ||= mail.attachments
+ .select { |attachment| attachment.filename.ends_with?('.patch') }
+ .sort_by(&:filename)
+ end
+
+ def source_branch
+ @source_branch ||= mail.subject
+ end
end
end
end
diff --git a/lib/gitlab/email/hook/additional_headers_interceptor.rb b/lib/gitlab/email/hook/additional_headers_interceptor.rb
index 064cb5e659a..aa2ef76069b 100644
--- a/lib/gitlab/email/hook/additional_headers_interceptor.rb
+++ b/lib/gitlab/email/hook/additional_headers_interceptor.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Email
module Hook
diff --git a/lib/gitlab/email/hook/delivery_metrics_observer.rb b/lib/gitlab/email/hook/delivery_metrics_observer.rb
index 1c2985f6045..c7af485fcc5 100644
--- a/lib/gitlab/email/hook/delivery_metrics_observer.rb
+++ b/lib/gitlab/email/hook/delivery_metrics_observer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Email
module Hook
diff --git a/lib/gitlab/email/hook/disable_email_interceptor.rb b/lib/gitlab/email/hook/disable_email_interceptor.rb
index 7bb8b53f0c8..6b6b1d85109 100644
--- a/lib/gitlab/email/hook/disable_email_interceptor.rb
+++ b/lib/gitlab/email/hook/disable_email_interceptor.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Email
module Hook
diff --git a/lib/gitlab/email/hook/email_template_interceptor.rb b/lib/gitlab/email/hook/email_template_interceptor.rb
index be0c4dd862e..13f8db2051d 100644
--- a/lib/gitlab/email/hook/email_template_interceptor.rb
+++ b/lib/gitlab/email/hook/email_template_interceptor.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Email
module Hook
diff --git a/lib/gitlab/email/html_parser.rb b/lib/gitlab/email/html_parser.rb
index 50559a48973..77f299bcade 100644
--- a/lib/gitlab/email/html_parser.rb
+++ b/lib/gitlab/email/html_parser.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Email
class HTMLParser
diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb
index cd9d3a6483f..ec412e7a8b1 100644
--- a/lib/gitlab/email/message/repository_push.rb
+++ b/lib/gitlab/email/message/repository_push.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Email
module Message
@@ -116,7 +118,7 @@ module Gitlab
end
def subject
- subject_text = '[Git]'
+ subject_text = ['[Git]']
subject_text << "[#{project.full_path}]"
subject_text << "[#{ref_name}]" if @action == :push
subject_text << ' '
@@ -134,6 +136,8 @@ module Gitlab
subject_action[0] = subject_action[0].capitalize
subject_text << "#{subject_action} #{ref_type} #{ref_name}"
end
+
+ subject_text.join
end
end
end
diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb
index d8c594ad0e7..d28f6b301fa 100644
--- a/lib/gitlab/email/receiver.rb
+++ b/lib/gitlab/email/receiver.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require_dependency 'gitlab/email/handler'
# Inspired in great part by Discourse's Email::Receiver
@@ -18,6 +20,7 @@ module Gitlab
InvalidIssueError = Class.new(InvalidRecordError)
InvalidMergeRequestError = Class.new(InvalidRecordError)
UnknownIncomingEmail = Class.new(ProcessingError)
+ InvalidAttachment = Class.new(ProcessingError)
class Receiver
def initialize(raw)
diff --git a/lib/gitlab/email/reply_parser.rb b/lib/gitlab/email/reply_parser.rb
index ae6b84607d6..2743f011ca6 100644
--- a/lib/gitlab/email/reply_parser.rb
+++ b/lib/gitlab/email/reply_parser.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Inspired in great part by Discourse's Email::Receiver
module Gitlab
module Email
diff --git a/lib/gitlab/etag_caching/middleware.rb b/lib/gitlab/etag_caching/middleware.rb
index d5d35dbd97f..0341f930b9c 100644
--- a/lib/gitlab/etag_caching/middleware.rb
+++ b/lib/gitlab/etag_caching/middleware.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module EtagCaching
class Middleware
diff --git a/lib/gitlab/etag_caching/router.rb b/lib/gitlab/etag_caching/router.rb
index 75167a6b088..08e30214b46 100644
--- a/lib/gitlab/etag_caching/router.rb
+++ b/lib/gitlab/etag_caching/router.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module EtagCaching
class Router
diff --git a/lib/gitlab/etag_caching/store.rb b/lib/gitlab/etag_caching/store.rb
index 21172ff8d93..2395e7be026 100644
--- a/lib/gitlab/etag_caching/store.rb
+++ b/lib/gitlab/etag_caching/store.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module EtagCaching
class Store
diff --git a/lib/gitlab/file_detector.rb b/lib/gitlab/file_detector.rb
index 4d89ee5a669..d6338b09e3d 100644
--- a/lib/gitlab/file_detector.rb
+++ b/lib/gitlab/file_detector.rb
@@ -8,7 +8,7 @@ module Gitlab
module FileDetector
PATTERNS = {
# Project files
- readme: %r{\Areadme[^/]*\z}i,
+ readme: %r{\A(readme|index)[^/]*\z}i,
changelog: %r{\A(changelog|history|changes|news)[^/]*\z}i,
license: %r{\A((un)?licen[sc]e|copying)(\.[^/]+)?\z}i,
contributing: %r{\Acontributing[^/]*\z}i,
diff --git a/lib/gitlab/gfm/reference_rewriter.rb b/lib/gitlab/gfm/reference_rewriter.rb
index 455814a9159..641446b52a5 100644
--- a/lib/gitlab/gfm/reference_rewriter.rb
+++ b/lib/gitlab/gfm/reference_rewriter.rb
@@ -31,19 +31,19 @@ module Gitlab
class ReferenceRewriter
RewriteError = Class.new(StandardError)
- def initialize(text, source_project, current_user)
+ def initialize(text, source_parent, current_user)
@text = text
- @source_project = source_project
+ @source_parent = source_parent
@current_user = current_user
@original_html = markdown(text)
@pattern = Gitlab::ReferenceExtractor.references_pattern
end
- def rewrite(target_project)
+ def rewrite(target_parent)
return @text unless needs_rewrite?
@text.gsub(@pattern) do |reference|
- unfold_reference(reference, Regexp.last_match, target_project)
+ unfold_reference(reference, Regexp.last_match, target_parent)
end
end
@@ -53,14 +53,14 @@ module Gitlab
private
- def unfold_reference(reference, match, target_project)
+ def unfold_reference(reference, match, target_parent)
before = @text[0...match.begin(0)]
after = @text[match.end(0)..-1]
referable = find_referable(reference)
return reference unless referable
- cross_reference = build_cross_reference(referable, target_project)
+ cross_reference = build_cross_reference(referable, target_parent)
return reference if reference == cross_reference
if cross_reference.nil?
@@ -72,17 +72,17 @@ module Gitlab
end
def find_referable(reference)
- extractor = Gitlab::ReferenceExtractor.new(@source_project,
+ extractor = Gitlab::ReferenceExtractor.new(@source_parent,
@current_user)
extractor.analyze(reference)
extractor.all.first
end
- def build_cross_reference(referable, target_project)
+ def build_cross_reference(referable, target_parent)
if referable.respond_to?(:project)
- referable.to_reference(target_project)
+ referable.to_reference(target_parent)
else
- referable.to_reference(@source_project, target_project: target_project)
+ referable.to_reference(@source_parent, target_project: target_parent)
end
end
@@ -91,7 +91,7 @@ module Gitlab
end
def markdown(text)
- Banzai.render(text, project: @source_project, no_original_data: true)
+ Banzai.render(text, project: @source_parent, no_original_data: true)
end
end
end
diff --git a/lib/gitlab/gfm/uploads_rewriter.rb b/lib/gitlab/gfm/uploads_rewriter.rb
index f7e66697da3..b767c8a278d 100644
--- a/lib/gitlab/gfm/uploads_rewriter.rb
+++ b/lib/gitlab/gfm/uploads_rewriter.rb
@@ -16,14 +16,15 @@ module Gitlab
@pattern = FileUploader::MARKDOWN_PATTERN
end
- def rewrite(target_project)
+ def rewrite(target_parent)
return @text unless needs_rewrite?
@text.gsub(@pattern) do |markdown|
file = find_file(@source_project, $~[:secret], $~[:file])
break markdown unless file.try(:exists?)
- moved = FileUploader.copy_to(file, target_project)
+ klass = target_parent.is_a?(Namespace) ? NamespaceFileUploader : FileUploader
+ moved = klass.copy_to(file, target_parent)
moved.markdown_link
end
end
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index 13b0bb930f4..0bd1d3420a2 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -5,6 +5,7 @@ module Gitlab
class Blob
include Gitlab::BlobHelper
include Gitlab::EncodingHelper
+ extend Gitlab::Git::WrapsGitalyErrors
# This number is the maximum amount of data that we want to display to
# the user. We load as much as we can for encoding detection and LFS
@@ -75,7 +76,7 @@ module Gitlab
# Returns array of Gitlab::Git::Blob
# Does not guarantee blob data will be set
def batch_lfs_pointers(repository, blob_ids)
- repository.wrapped_gitaly_errors do
+ wrapped_gitaly_errors do
repository.gitaly_blob_client.batch_lfs_pointers(blob_ids.to_a)
end
end
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index 74cdabfed9d..2820491b65d 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -3,6 +3,7 @@ module Gitlab
module Git
class Commit
include Gitlab::EncodingHelper
+ extend Gitlab::Git::WrapsGitalyErrors
attr_accessor :raw_commit, :head
@@ -59,7 +60,7 @@ module Gitlab
# This saves us an RPC round trip.
return nil if commit_id.include?(':')
- commit = repo.wrapped_gitaly_errors do
+ commit = wrapped_gitaly_errors do
repo.gitaly_commit_client.find_commit(commit_id)
end
@@ -100,7 +101,7 @@ module Gitlab
# Commit.between(repo, '29eda46b', 'master')
#
def between(repo, base, head)
- repo.wrapped_gitaly_errors do
+ wrapped_gitaly_errors do
repo.gitaly_commit_client.between(base, head)
end
end
@@ -125,7 +126,7 @@ module Gitlab
# are documented here:
# http://www.rubydoc.info/github/libgit2/rugged/Rugged#SORT_NONE-constant)
def find_all(repo, options = {})
- repo.wrapped_gitaly_errors do
+ wrapped_gitaly_errors do
Gitlab::GitalyClient::CommitService.new(repo).find_all_commits(options)
end
end
@@ -142,7 +143,7 @@ module Gitlab
# relation to each other. The last 10 commits for a branch for example,
# should go through .where
def batch_by_oid(repo, oids)
- repo.wrapped_gitaly_errors do
+ wrapped_gitaly_errors do
repo.gitaly_commit_client.list_commits_by_oid(oids)
end
end
diff --git a/lib/gitlab/git/commit_stats.rb b/lib/gitlab/git/commit_stats.rb
index ae6f554bc06..83a9fd5f81a 100644
--- a/lib/gitlab/git/commit_stats.rb
+++ b/lib/gitlab/git/commit_stats.rb
@@ -3,6 +3,8 @@
module Gitlab
module Git
class CommitStats
+ include Gitlab::Git::WrapsGitalyErrors
+
attr_reader :id, :additions, :deletions, :total
# Instantiate a CommitStats object
@@ -14,7 +16,7 @@ module Gitlab
@deletions = 0
@total = 0
- repo.wrapped_gitaly_errors do
+ wrapped_gitaly_errors do
gitaly_stats(repo, commit)
end
end
diff --git a/lib/gitlab/git/conflict/resolver.rb b/lib/gitlab/git/conflict/resolver.rb
index 6dc792c16b8..307f1b8cb66 100644
--- a/lib/gitlab/git/conflict/resolver.rb
+++ b/lib/gitlab/git/conflict/resolver.rb
@@ -2,6 +2,8 @@ module Gitlab
module Git
module Conflict
class Resolver
+ include Gitlab::Git::WrapsGitalyErrors
+
ConflictSideMissing = Class.new(StandardError)
ResolutionError = Class.new(StandardError)
@@ -12,7 +14,7 @@ module Gitlab
end
def conflicts
- @conflicts ||= @target_repository.wrapped_gitaly_errors do
+ @conflicts ||= wrapped_gitaly_errors do
gitaly_conflicts_client(@target_repository).list_conflict_files.to_a
end
rescue GRPC::FailedPrecondition => e
@@ -22,7 +24,7 @@ module Gitlab
end
def resolve_conflicts(source_repository, resolution, source_branch:, target_branch:)
- source_repository.wrapped_gitaly_errors do
+ wrapped_gitaly_errors do
gitaly_conflicts_client(source_repository).resolve_conflicts(@target_repository, resolution, source_branch, target_branch)
end
end
diff --git a/lib/gitlab/git/patches/collection.rb b/lib/gitlab/git/patches/collection.rb
new file mode 100644
index 00000000000..ad6b5d32abc
--- /dev/null
+++ b/lib/gitlab/git/patches/collection.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Git
+ module Patches
+ class Collection
+ MAX_PATCH_SIZE = 2.megabytes
+
+ def initialize(one_or_more_patches)
+ @patches = Array(one_or_more_patches).map do |patch_content|
+ Gitlab::Git::Patches::Patch.new(patch_content)
+ end
+ end
+
+ def content
+ @patches.map(&:content).join("\n")
+ end
+
+ def valid_size?
+ size < MAX_PATCH_SIZE
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ # `@patches` is not an `ActiveRecord` relation, but an `Enumerable`
+ # We're using sum from `ActiveSupport`
+ def size
+ @size ||= @patches.sum(&:size)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/patches/commit_patches.rb b/lib/gitlab/git/patches/commit_patches.rb
new file mode 100644
index 00000000000..c62994432d3
--- /dev/null
+++ b/lib/gitlab/git/patches/commit_patches.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Git
+ module Patches
+ class CommitPatches
+ include Gitlab::Git::WrapsGitalyErrors
+
+ def initialize(user, repository, branch, patch_collection)
+ @user, @repository, @branch, @patches = user, repository, branch, patch_collection
+ end
+
+ def commit
+ repository.with_cache_hooks do
+ wrapped_gitaly_errors do
+ operation_service.user_commit_patches(user, branch, patches.content)
+ end
+ end
+ end
+
+ private
+
+ attr_reader :user, :repository, :branch, :patches
+
+ def operation_service
+ repository.raw.gitaly_operation_client
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/patches/patch.rb b/lib/gitlab/git/patches/patch.rb
new file mode 100644
index 00000000000..fe6ae1b5b00
--- /dev/null
+++ b/lib/gitlab/git/patches/patch.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Git
+ module Patches
+ class Patch
+ attr_reader :content
+
+ def initialize(content)
+ @content = content
+ end
+
+ def size
+ content.bytesize
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/remote_mirror.rb b/lib/gitlab/git/remote_mirror.rb
index e4743b4db0a..7f9520de5ce 100644
--- a/lib/gitlab/git/remote_mirror.rb
+++ b/lib/gitlab/git/remote_mirror.rb
@@ -1,13 +1,15 @@
module Gitlab
module Git
class RemoteMirror
+ include Gitlab::Git::WrapsGitalyErrors
+
def initialize(repository, ref_name)
@repository = repository
@ref_name = ref_name
end
def update(only_branches_matching: [])
- @repository.wrapped_gitaly_errors do
+ wrapped_gitaly_errors do
@repository.gitaly_remote_client.update_remote_mirror(@ref_name, only_branches_matching)
end
end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 9df04372cc2..aaaf794668d 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -6,6 +6,7 @@ module Gitlab
module Git
class Repository
include Gitlab::Git::RepositoryMirroring
+ include Gitlab::Git::WrapsGitalyErrors
include Gitlab::EncodingHelper
include Gitlab::Utils::StrongMemoize
@@ -418,13 +419,17 @@ module Gitlab
end
def diff_stats(left_id, right_id)
+ if [left_id, right_id].any? { |ref| ref.blank? || Gitlab::Git.blank_ref?(ref) }
+ return empty_diff_stats
+ end
+
stats = wrapped_gitaly_errors do
gitaly_commit_client.diff_stats(left_id, right_id)
end
Gitlab::Git::DiffStatsCollection.new(stats)
rescue CommandError, TypeError
- Gitlab::Git::DiffStatsCollection.new([])
+ empty_diff_stats
end
# Returns a RefName for a given SHA
@@ -570,6 +575,20 @@ module Gitlab
end
end
+ def update_submodule(user:, submodule:, commit_sha:, message:, branch:)
+ args = {
+ user: user,
+ submodule: submodule,
+ commit_sha: commit_sha,
+ branch: branch,
+ message: message
+ }
+
+ wrapped_gitaly_errors do
+ gitaly_operation_client.user_update_submodule(args)
+ end
+ end
+
# Delete the specified branch from the repository
def delete_branch(branch_name)
wrapped_gitaly_errors do
@@ -719,6 +738,26 @@ module Gitlab
end
end
+ # Fetch remote for repository
+ #
+ # remote - remote name
+ # ssh_auth - SSH known_hosts data and a private key to use for public-key authentication
+ # forced - should we use --force flag?
+ # no_tags - should we use --no-tags flag?
+ # prune - should we use --prune flag?
+ def fetch_remote(remote, ssh_auth: nil, forced: false, no_tags: false, prune: true)
+ wrapped_gitaly_errors do
+ gitaly_repository_client.fetch_remote(
+ remote,
+ ssh_auth: ssh_auth,
+ forced: forced,
+ no_tags: no_tags,
+ prune: prune,
+ timeout: GITLAB_PROJECTS_TIMEOUT
+ )
+ end
+ end
+
def blob_at(sha, path)
Gitlab::Git::Blob.find(self, sha, path) unless Gitlab::Git.blank_ref?(sha)
end
@@ -845,23 +884,9 @@ module Gitlab
end
def gitaly_migrate(method, status: Gitlab::GitalyClient::MigrationStatus::OPT_IN, &block)
- Gitlab::GitalyClient.migrate(method, status: status, &block)
- rescue GRPC::NotFound => e
- raise NoRepository.new(e)
- rescue GRPC::InvalidArgument => e
- raise ArgumentError.new(e)
- rescue GRPC::BadStatus => e
- raise CommandError.new(e)
- end
-
- def wrapped_gitaly_errors(&block)
- yield block
- rescue GRPC::NotFound => e
- raise NoRepository.new(e)
- rescue GRPC::InvalidArgument => e
- raise ArgumentError.new(e)
- rescue GRPC::BadStatus => e
- raise CommandError.new(e)
+ wrapped_gitaly_errors do
+ Gitlab::GitalyClient.migrate(method, status: status, &block)
+ end
end
def clean_stale_repository_files
@@ -941,6 +966,10 @@ module Gitlab
private
+ def empty_diff_stats
+ Gitlab::Git::DiffStatsCollection.new([])
+ end
+
def uncached_has_local_branches?
wrapped_gitaly_errors do
gitaly_repository_client.has_local_branches?
diff --git a/lib/gitlab/git/tree.rb b/lib/gitlab/git/tree.rb
index e0867aeb5a7..b5b701699f0 100644
--- a/lib/gitlab/git/tree.rb
+++ b/lib/gitlab/git/tree.rb
@@ -2,6 +2,7 @@ module Gitlab
module Git
class Tree
include Gitlab::EncodingHelper
+ extend Gitlab::Git::WrapsGitalyErrors
attr_accessor :id, :root_id, :name, :path, :flat_path, :type,
:mode, :commit_id, :submodule_url
@@ -15,7 +16,7 @@ module Gitlab
def where(repository, sha, path = nil, recursive = false)
path = nil if path == '' || path == '/'
- repository.wrapped_gitaly_errors do
+ wrapped_gitaly_errors do
repository.gitaly_commit_client.tree_entries(repository, sha, path, recursive)
end
end
diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb
index 7fe56979d5c..02c643d0da0 100644
--- a/lib/gitlab/git/wiki.rb
+++ b/lib/gitlab/git/wiki.rb
@@ -1,6 +1,8 @@
module Gitlab
module Git
class Wiki
+ include Gitlab::Git::WrapsGitalyErrors
+
DuplicatePageError = Class.new(StandardError)
OperationError = Class.new(StandardError)
@@ -65,37 +67,37 @@ module Gitlab
end
def write_page(name, format, content, commit_details)
- @repository.wrapped_gitaly_errors do
+ wrapped_gitaly_errors do
gitaly_write_page(name, format, content, commit_details)
end
end
def delete_page(page_path, commit_details)
- @repository.wrapped_gitaly_errors do
+ wrapped_gitaly_errors do
gitaly_delete_page(page_path, commit_details)
end
end
def update_page(page_path, title, format, content, commit_details)
- @repository.wrapped_gitaly_errors do
+ wrapped_gitaly_errors do
gitaly_update_page(page_path, title, format, content, commit_details)
end
end
def pages(limit: 0)
- @repository.wrapped_gitaly_errors do
+ wrapped_gitaly_errors do
gitaly_get_all_pages(limit: limit)
end
end
def page(title:, version: nil, dir: nil)
- @repository.wrapped_gitaly_errors do
+ wrapped_gitaly_errors do
gitaly_find_page(title: title, version: version, dir: dir)
end
end
def file(name, version)
- @repository.wrapped_gitaly_errors do
+ wrapped_gitaly_errors do
gitaly_find_file(name, version)
end
end
@@ -105,7 +107,7 @@ module Gitlab
# :per_page - The number of items per page.
# :limit - Total number of items to return.
def page_versions(page_path, options = {})
- versions = @repository.wrapped_gitaly_errors do
+ versions = wrapped_gitaly_errors do
gitaly_wiki_client.page_versions(page_path, options)
end
@@ -127,7 +129,7 @@ module Gitlab
def page_formatted_data(title:, dir: nil, version: nil)
version = version&.id
- @repository.wrapped_gitaly_errors do
+ wrapped_gitaly_errors do
gitaly_wiki_client.get_formatted_data(title: title, dir: dir, version: version)
end
end
diff --git a/lib/gitlab/git/wraps_gitaly_errors.rb b/lib/gitlab/git/wraps_gitaly_errors.rb
new file mode 100644
index 00000000000..4b161f7e6ce
--- /dev/null
+++ b/lib/gitlab/git/wraps_gitaly_errors.rb
@@ -0,0 +1,15 @@
+module Gitlab
+ module Git
+ module WrapsGitalyErrors
+ def wrapped_gitaly_errors(&block)
+ yield block
+ rescue GRPC::NotFound => e
+ raise Gitlab::Git::Repository::NoRepository.new(e)
+ rescue GRPC::InvalidArgument => e
+ raise ArgumentError.new(e)
+ rescue GRPC::BadStatus => e
+ raise Gitlab::Git::CommandError.new(e)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb
index 0f148614b20..4c78b790ce5 100644
--- a/lib/gitlab/gitaly_client/operation_service.rb
+++ b/lib/gitlab/gitaly_client/operation_service.rb
@@ -230,6 +230,32 @@ module Gitlab
response.squash_sha
end
+ def user_update_submodule(user:, submodule:, commit_sha:, branch:, message:)
+ request = Gitaly::UserUpdateSubmoduleRequest.new(
+ repository: @gitaly_repo,
+ user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
+ commit_sha: commit_sha,
+ branch: encode_binary(branch),
+ submodule: encode_binary(submodule),
+ commit_message: encode_binary(message)
+ )
+
+ response = GitalyClient.call(
+ @repository.storage,
+ :operation_service,
+ :user_update_submodule,
+ request
+ )
+
+ if response.pre_receive_error.present?
+ raise Gitlab::Git::PreReceiveError, response.pre_receive_error
+ elsif response.commit_error.present?
+ raise Gitlab::Git::CommitError, response.commit_error
+ else
+ Gitlab::Git::OperationService::BranchUpdate.from_gitaly(response.branch_update)
+ end
+ end
+
def user_commit_files(
user, branch_name, commit_message, actions, author_email, author_name,
start_branch_name, start_repository)
@@ -273,6 +299,29 @@ module Gitlab
Gitlab::Git::OperationService::BranchUpdate.from_gitaly(response.branch_update)
end
+ def user_commit_patches(user, branch_name, patches)
+ header = Gitaly::UserApplyPatchRequest::Header.new(
+ repository: @gitaly_repo,
+ user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
+ target_branch: encode_binary(branch_name)
+ )
+ reader = binary_stringio(patches)
+
+ chunks = Enumerator.new do |chunk|
+ chunk.yield Gitaly::UserApplyPatchRequest.new(header: header)
+
+ until reader.eof?
+ patch_chunk = reader.read(MAX_MSG_SIZE)
+
+ chunk.yield(Gitaly::UserApplyPatchRequest.new(patches: patch_chunk))
+ end
+ end
+
+ response = GitalyClient.call(@repository.storage, :operation_service, :user_apply_patch, chunks)
+
+ Gitlab::Git::OperationService::BranchUpdate.from_gitaly(response.branch_update)
+ end
+
private
def call_cherry_pick_or_revert(rpc, user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb
index 2956ed4b911..d7b36946b65 100644
--- a/lib/gitlab/gitaly_client/repository_service.rb
+++ b/lib/gitlab/gitaly_client/repository_service.rb
@@ -349,7 +349,7 @@ module Gitlab
f.write(message.data)
end
end
- # If the file is empty means that we recieved an empty stream, we delete the file
+ # If the file is empty means that we received an empty stream, we delete the file
FileUtils.rm(save_path) if File.zero?(save_path)
end
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 860c39feb64..15137140639 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -8,7 +8,10 @@ module Gitlab
def add_gon_variables
gon.api_version = 'v4'
- gon.default_avatar_url = URI.join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s
+ gon.default_avatar_url =
+ Gitlab::Utils.append_path(
+ Gitlab.config.gitlab.url,
+ ActionController::Base.helpers.image_path('no_avatar.png'))
gon.max_file_size = Gitlab::CurrentSettings.max_attachment_size
gon.asset_host = ActionController::Base.asset_host
gon.webpack_public_path = webpack_public_path
diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb
index 83095acc528..a4e60bbd828 100644
--- a/lib/gitlab/highlight.rb
+++ b/lib/gitlab/highlight.rb
@@ -4,22 +4,25 @@ module Gitlab
class Highlight
TIMEOUT_BACKGROUND = 30.seconds
TIMEOUT_FOREGROUND = 3.seconds
+ MAXIMUM_TEXT_HIGHLIGHT_SIZE = 1.megabyte
- def self.highlight(blob_name, blob_content, repository: nil, plain: false)
- new(blob_name, blob_content, repository: repository)
+ def self.highlight(blob_name, blob_content, language: nil, plain: false)
+ new(blob_name, blob_content, language: language)
.highlight(blob_content, continue: false, plain: plain)
end
attr_reader :blob_name
- def initialize(blob_name, blob_content, repository: nil)
+ def initialize(blob_name, blob_content, language: nil)
@formatter = Rouge::Formatters::HTMLGitlab
- @repository = repository
+ @language = language
@blob_name = blob_name
@blob_content = blob_content
end
def highlight(text, continue: true, plain: false)
+ plain ||= text.length > MAXIMUM_TEXT_HIGHLIGHT_SIZE
+
highlighted_text = highlight_text(text, continue: continue, plain: plain)
highlighted_text = link_dependencies(text, highlighted_text) if blob_name
highlighted_text
@@ -36,11 +39,9 @@ module Gitlab
private
def custom_language
- language_name = @repository && @repository.gitattribute(@blob_name, 'gitlab-language')
-
- return nil unless language_name
+ return nil unless @language
- Rouge::Lexer.find_fancy(language_name)
+ Rouge::Lexer.find_fancy(@language)
end
def highlight_text(text, continue: true, plain: false)
diff --git a/lib/gitlab/import/merge_request_helpers.rb b/lib/gitlab/import/merge_request_helpers.rb
index 97dc1a987c4..9215067d973 100644
--- a/lib/gitlab/import/merge_request_helpers.rb
+++ b/lib/gitlab/import/merge_request_helpers.rb
@@ -22,7 +22,7 @@ module Gitlab
# additional work that is strictly necessary.
merge_request_id = insert_and_return_id(attributes, project.merge_requests)
- merge_request = project.merge_requests.find(merge_request_id)
+ merge_request = project.merge_requests.reload.find(merge_request_id)
# We use .insert_and_return_id which effectively disables all callbacks.
# Trigger iid logic here to make sure we track internal id values consistently.
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 2bed470514b..9790818ecaf 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -92,6 +92,7 @@ excluded_attributes:
- :path
- :namespace_id
- :creator_id
+ - :pool_repository_id
- :import_url
- :import_status
- :avatar
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index 3d693d23c99..99581eb0416 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -154,7 +154,7 @@ module Gitlab
Project.transaction do
process_sub_relation(relation, relation_item)
- # For every subrelation that hangs from Project, save the associated records alltogether
+ # For every subrelation that hangs from Project, save the associated records altogether
# This effectively batches all records per subrelation item, only keeping those in memory
# We have to keep in mind that more batch granularity << Memory, but >> Slowness
if save
diff --git a/lib/gitlab/kubernetes/helm.rb b/lib/gitlab/kubernetes/helm.rb
index 4a1bdf34c3e..1cd4f9e17b7 100644
--- a/lib/gitlab/kubernetes/helm.rb
+++ b/lib/gitlab/kubernetes/helm.rb
@@ -2,6 +2,7 @@ module Gitlab
module Kubernetes
module Helm
HELM_VERSION = '2.7.2'.freeze
+ KUBECTL_VERSION = '1.11.0'.freeze
NAMESPACE = 'gitlab-managed-apps'.freeze
SERVICE_ACCOUNT = 'tiller'.freeze
CLUSTER_ROLE_BINDING = 'tiller-admin'.freeze
diff --git a/lib/gitlab/kubernetes/helm/api.rb b/lib/gitlab/kubernetes/helm/api.rb
index e21bc531444..7c026ac9e68 100644
--- a/lib/gitlab/kubernetes/helm/api.rb
+++ b/lib/gitlab/kubernetes/helm/api.rb
@@ -54,7 +54,11 @@ module Gitlab
def create_config_map(command)
command.config_map_resource.tap do |config_map_resource|
- kubeclient.create_config_map(config_map_resource)
+ if config_map_exists?(config_map_resource)
+ kubeclient.update_config_map(config_map_resource)
+ else
+ kubeclient.create_config_map(config_map_resource)
+ end
end
end
@@ -88,23 +92,21 @@ module Gitlab
end
end
- def service_account_exists?(resource)
- resource_exists? do
- kubeclient.get_service_account(resource.metadata.name, resource.metadata.namespace)
- end
+ def config_map_exists?(resource)
+ kubeclient.get_config_map(resource.metadata.name, resource.metadata.namespace)
+ rescue ::Kubeclient::ResourceNotFoundError
+ false
end
- def cluster_role_binding_exists?(resource)
- resource_exists? do
- kubeclient.get_cluster_role_binding(resource.metadata.name)
- end
+ def service_account_exists?(resource)
+ kubeclient.get_service_account(resource.metadata.name, resource.metadata.namespace)
+ rescue ::Kubeclient::ResourceNotFoundError
+ false
end
- def resource_exists?
- yield
- rescue ::Kubeclient::HttpError => e
- raise e unless e.error_code == 404
-
+ def cluster_role_binding_exists?(resource)
+ kubeclient.get_cluster_role_binding(resource.metadata.name)
+ rescue ::Kubeclient::ResourceNotFoundError
false
end
end
diff --git a/lib/gitlab/kubernetes/helm/base_command.rb b/lib/gitlab/kubernetes/helm/base_command.rb
index 6752f2cff43..f2ce24fefa1 100644
--- a/lib/gitlab/kubernetes/helm/base_command.rb
+++ b/lib/gitlab/kubernetes/helm/base_command.rb
@@ -10,13 +10,7 @@ module Gitlab
def generate_script
<<~HEREDOC
- set -eo pipefail
- ALPINE_VERSION=$(cat /etc/alpine-release | cut -d '.' -f 1,2)
- echo http://mirror.clarkson.edu/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories
- echo http://mirror1.hs-esslingen.de/pub/Mirrors/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories
- apk add -U wget ca-certificates openssl >/dev/null
- wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v#{Gitlab::Kubernetes::Helm::HELM_VERSION}-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
- mv /tmp/linux-amd64/helm /usr/bin/
+ set -xeo pipefail
HEREDOC
end
diff --git a/lib/gitlab/kubernetes/helm/init_command.rb b/lib/gitlab/kubernetes/helm/init_command.rb
index c7046a9ea75..6691080deca 100644
--- a/lib/gitlab/kubernetes/helm/init_command.rb
+++ b/lib/gitlab/kubernetes/helm/init_command.rb
@@ -45,7 +45,7 @@ module Gitlab
def init_helm_command
command = %w[helm init] + init_command_flags
- command.shelljoin + " >/dev/null\n"
+ command.shelljoin
end
def init_command_flags
diff --git a/lib/gitlab/kubernetes/helm/install_command.rb b/lib/gitlab/kubernetes/helm/install_command.rb
index 1be7924d6ac..ff1c1657b98 100644
--- a/lib/gitlab/kubernetes/helm/install_command.rb
+++ b/lib/gitlab/kubernetes/helm/install_command.rb
@@ -4,22 +4,27 @@ module Gitlab
class InstallCommand
include BaseCommand
- attr_reader :name, :files, :chart, :version, :repository
+ attr_reader :name, :files, :chart, :version, :repository, :preinstall, :postinstall
- def initialize(name:, chart:, files:, rbac:, version: nil, repository: nil)
+ def initialize(name:, chart:, files:, rbac:, version: nil, repository: nil, preinstall: nil, postinstall: nil)
@name = name
@chart = chart
@version = version
@rbac = rbac
@files = files
@repository = repository
+ @preinstall = preinstall
+ @postinstall = postinstall
end
def generate_script
super + [
init_command,
repository_command,
- script_command
+ repository_update_command,
+ preinstall_command,
+ install_command,
+ postinstall_command
].compact.join("\n")
end
@@ -30,17 +35,29 @@ module Gitlab
private
def init_command
- 'helm init --client-only >/dev/null'
+ 'helm init --client-only'
end
def repository_command
['helm', 'repo', 'add', name, repository].shelljoin if repository
end
- def script_command
+ def repository_update_command
+ 'helm repo update' if repository
+ end
+
+ def install_command
command = ['helm', 'install', chart] + install_command_flags
- command.shelljoin + " >/dev/null\n"
+ command.shelljoin
+ end
+
+ def preinstall_command
+ preinstall.join("\n") if preinstall
+ end
+
+ def postinstall_command
+ postinstall.join("\n") if postinstall
end
def install_command_flags
diff --git a/lib/gitlab/kubernetes/helm/pod.rb b/lib/gitlab/kubernetes/helm/pod.rb
index 95192b11c0d..e9c621d96f0 100644
--- a/lib/gitlab/kubernetes/helm/pod.rb
+++ b/lib/gitlab/kubernetes/helm/pod.rb
@@ -25,7 +25,7 @@ module Gitlab
def container_specification
{
name: 'helm',
- image: 'alpine:3.6',
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/helm-install-image/releases/#{Gitlab::Kubernetes::Helm::HELM_VERSION}-kube-#{Gitlab::Kubernetes::Helm::KUBECTL_VERSION}",
env: generate_pod_env(command),
command: %w(/bin/sh),
args: %w(-c $(COMMAND_SCRIPT))
diff --git a/lib/gitlab/kubernetes/helm/upgrade_command.rb b/lib/gitlab/kubernetes/helm/upgrade_command.rb
index 74188046739..b36315f7a82 100644
--- a/lib/gitlab/kubernetes/helm/upgrade_command.rb
+++ b/lib/gitlab/kubernetes/helm/upgrade_command.rb
@@ -36,7 +36,7 @@ module Gitlab
private
def init_command
- 'helm init --client-only >/dev/null'
+ 'helm init --client-only'
end
def repository_command
@@ -50,7 +50,7 @@ module Gitlab
" --namespace #{::Gitlab::Kubernetes::Helm::NAMESPACE}" \
" -f /data/helm/#{name}/config/values.yaml"
- "helm upgrade #{name} #{chart}#{upgrade_flags} >/dev/null\n"
+ "helm upgrade #{name} #{chart}#{upgrade_flags}"
end
def optional_version_flag
diff --git a/lib/gitlab/kubernetes/kube_client.rb b/lib/gitlab/kubernetes/kube_client.rb
index f266177bec1..b947f6b551e 100644
--- a/lib/gitlab/kubernetes/kube_client.rb
+++ b/lib/gitlab/kubernetes/kube_client.rb
@@ -16,7 +16,8 @@ module Gitlab
SUPPORTED_API_GROUPS = {
core: { group: 'api', version: 'v1' },
rbac: { group: 'apis/rbac.authorization.k8s.io', version: 'v1' },
- extensions: { group: 'apis/extensions', version: 'v1beta1' }
+ extensions: { group: 'apis/extensions', version: 'v1beta1' },
+ knative: { group: 'apis/serving.knative.dev', version: 'v1alpha1' }
}.freeze
SUPPORTED_API_GROUPS.each do |name, params|
diff --git a/lib/gitlab/kubernetes/namespace.rb b/lib/gitlab/kubernetes/namespace.rb
index e6ff6160ab9..783c8a24741 100644
--- a/lib/gitlab/kubernetes/namespace.rb
+++ b/lib/gitlab/kubernetes/namespace.rb
@@ -10,9 +10,7 @@ module Gitlab
def exists?
@client.get_namespace(name)
- rescue ::Kubeclient::HttpError => ke
- raise ke unless ke.error_code == 404
-
+ rescue ::Kubeclient::ResourceNotFoundError
false
end
diff --git a/lib/gitlab/kubernetes/role_binding.rb b/lib/gitlab/kubernetes/role_binding.rb
index 4f3ee040bf2..cb0cb42d007 100644
--- a/lib/gitlab/kubernetes/role_binding.rb
+++ b/lib/gitlab/kubernetes/role_binding.rb
@@ -3,9 +3,8 @@
module Gitlab
module Kubernetes
class RoleBinding
- attr_reader :role_name, :namespace, :service_account_name
-
- def initialize(role_name:, namespace:, service_account_name:)
+ def initialize(name:, role_name:, namespace:, service_account_name:)
+ @name = name
@role_name = role_name
@namespace = namespace
@service_account_name = service_account_name
@@ -21,14 +20,16 @@ module Gitlab
private
+ attr_reader :name, :role_name, :namespace, :service_account_name
+
def metadata
- { name: "gitlab-#{namespace}", namespace: namespace }
+ { name: name, namespace: namespace }
end
def role_ref
{
apiGroup: 'rbac.authorization.k8s.io',
- kind: 'Role',
+ kind: 'ClusterRole',
name: role_name
}
end
diff --git a/lib/gitlab/manifest_import/manifest.rb b/lib/gitlab/manifest_import/manifest.rb
index 4d6034fb956..b69b9ac4b64 100644
--- a/lib/gitlab/manifest_import/manifest.rb
+++ b/lib/gitlab/manifest_import/manifest.rb
@@ -63,7 +63,7 @@ module Gitlab
end
def repository_url(name)
- URI.join(remote, name).to_s
+ Gitlab::Utils.append_path(remote, name)
end
def remote
diff --git a/lib/gitlab/markup_helper.rb b/lib/gitlab/markup_helper.rb
index 142b7d1a472..d419fa66e57 100644
--- a/lib/gitlab/markup_helper.rb
+++ b/lib/gitlab/markup_helper.rb
@@ -4,10 +4,11 @@ module Gitlab
module MarkupHelper
extend self
- MARKDOWN_EXTENSIONS = %w(mdown mkd mkdn md markdown).freeze
- ASCIIDOC_EXTENSIONS = %w(adoc ad asciidoc).freeze
- OTHER_EXTENSIONS = %w(textile rdoc org creole wiki mediawiki rst).freeze
+ MARKDOWN_EXTENSIONS = %w[mdown mkd mkdn md markdown].freeze
+ ASCIIDOC_EXTENSIONS = %w[adoc ad asciidoc].freeze
+ OTHER_EXTENSIONS = %w[textile rdoc org creole wiki mediawiki rst].freeze
EXTENSIONS = MARKDOWN_EXTENSIONS + ASCIIDOC_EXTENSIONS + OTHER_EXTENSIONS
+ PLAIN_FILENAMES = %w[readme index].freeze
# Public: Determines if a given filename is compatible with GitHub::Markup.
#
@@ -43,7 +44,7 @@ module Gitlab
#
# Returns boolean
def plain?(filename)
- extension(filename) == 'txt' || filename.casecmp('readme').zero?
+ extension(filename) == 'txt' || plain_filename?(filename)
end
def previewable?(filename)
@@ -55,5 +56,9 @@ module Gitlab
def extension(filename)
File.extname(filename).downcase.delete('.')
end
+
+ def plain_filename?(filename)
+ PLAIN_FILENAMES.include?(filename.downcase)
+ end
end
end
diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb
index 1fd8f147b44..6943567fb6d 100644
--- a/lib/gitlab/middleware/go.rb
+++ b/lib/gitlab/middleware/go.rb
@@ -38,7 +38,7 @@ module Gitlab
def go_body(path)
config = Gitlab.config
- project_url = URI.join(config.gitlab.url, path)
+ project_url = Gitlab::Utils.append_path(config.gitlab.url, path)
import_prefix = strip_url(project_url.to_s)
repository_url = if Gitlab::CurrentSettings.enabled_git_access_protocol == 'ssh'
diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb
index 44025650de0..fa68dead80b 100644
--- a/lib/gitlab/path_regex.rb
+++ b/lib/gitlab/path_regex.rb
@@ -236,7 +236,7 @@ module Gitlab
def single_line_regexp(regex)
# Turns a multiline extended regexp into a single line one,
- # beacuse `rake routes` breaks on multiline regexes.
+ # because `rake routes` breaks on multiline regexes.
Regexp.new(regex.source.gsub(/\(\?#.+?\)/, '').gsub(/\s*/, ''), regex.options ^ Regexp::EXTENDED).freeze
end
end
diff --git a/lib/gitlab/private_commit_email.rb b/lib/gitlab/private_commit_email.rb
new file mode 100644
index 00000000000..bade2248ccd
--- /dev/null
+++ b/lib/gitlab/private_commit_email.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module PrivateCommitEmail
+ TOKEN = "_private".freeze
+
+ class << self
+ def regex
+ hostname_regexp = Regexp.escape(Gitlab::CurrentSettings.current_application_settings.commit_email_hostname)
+
+ /\A(?<id>([0-9]+))\-([^@]+)@#{hostname_regexp}\z/
+ end
+
+ def user_id_for_email(email)
+ match = email&.match(regex)
+ return unless match
+
+ match[:id].to_i
+ end
+
+ def for_user(user)
+ hostname = Gitlab::CurrentSettings.current_application_settings.commit_email_hostname
+
+ "#{user.id}-#{user.username}@#{hostname}"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 3a202d915e3..04df881bf03 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -82,7 +82,7 @@ module Gitlab
ref: ref,
startline: startline,
data: data.join,
- project_id: project ? project.id : nil
+ project: project
)
end
diff --git a/lib/gitlab/proxy_http_connection_adapter.rb b/lib/gitlab/proxy_http_connection_adapter.rb
index 82213098672..a64cb47e77e 100644
--- a/lib/gitlab/proxy_http_connection_adapter.rb
+++ b/lib/gitlab/proxy_http_connection_adapter.rb
@@ -4,7 +4,7 @@
# of the global setting allow_local_requests_from_hooks_and_services this adapter
# will allow/block connection to internal IPs and/or urls.
#
-# This functionality can be overriden by providing the setting the option
+# This functionality can be overridden by providing the setting the option
# allow_local_requests = true in the request. For example:
# Gitlab::HTTP.get('http://www.gitlab.com', allow_local_requests: true)
#
diff --git a/lib/gitlab/quick_actions/command_definition.rb b/lib/gitlab/quick_actions/command_definition.rb
index 96415271316..c682eb22890 100644
--- a/lib/gitlab/quick_actions/command_definition.rb
+++ b/lib/gitlab/quick_actions/command_definition.rb
@@ -2,13 +2,14 @@ module Gitlab
module QuickActions
class CommandDefinition
attr_accessor :name, :aliases, :description, :explanation, :params,
- :condition_block, :parse_params_block, :action_block
+ :condition_block, :parse_params_block, :action_block, :warning
def initialize(name, attributes = {})
@name = name
@aliases = attributes[:aliases] || []
@description = attributes[:description] || ''
+ @warning = attributes[:warning] || ''
@explanation = attributes[:explanation] || ''
@params = attributes[:params] || []
@condition_block = attributes[:condition_block]
@@ -33,11 +34,13 @@ module Gitlab
def explain(context, arg)
return unless available?(context)
- if explanation.respond_to?(:call)
- execute_block(explanation, context, arg)
- else
- explanation
- end
+ message = if explanation.respond_to?(:call)
+ execute_block(explanation, context, arg)
+ else
+ explanation
+ end
+
+ warning.empty? ? message : "#{message} (#{warning})"
end
def execute(context, arg)
@@ -61,6 +64,7 @@ module Gitlab
name: name,
aliases: aliases,
description: desc,
+ warning: warning,
params: prms
}
end
diff --git a/lib/gitlab/quick_actions/dsl.rb b/lib/gitlab/quick_actions/dsl.rb
index d82dccd0db5..192c7ec2ff5 100644
--- a/lib/gitlab/quick_actions/dsl.rb
+++ b/lib/gitlab/quick_actions/dsl.rb
@@ -31,6 +31,10 @@ module Gitlab
@description = block_given? ? block : text
end
+ def warning(message = '')
+ @warning = message
+ end
+
# Allows to define params for the next quick action.
# These params are shown in the autocomplete menu.
#
@@ -133,6 +137,7 @@ module Gitlab
name,
aliases: aliases,
description: @description,
+ warning: @warning,
explanation: @explanation,
params: @params,
condition_block: @condition_block,
@@ -150,6 +155,7 @@ module Gitlab
@explanation = nil
@params = nil
@condition_block = nil
+ @warning = nil
@parse_params_block = nil
end
end
diff --git a/lib/gitlab/quick_actions/extractor.rb b/lib/gitlab/quick_actions/extractor.rb
index 30c6806b68e..59f8dd889aa 100644
--- a/lib/gitlab/quick_actions/extractor.rb
+++ b/lib/gitlab/quick_actions/extractor.rb
@@ -29,7 +29,7 @@ module Gitlab
# commands = extractor.extract_commands(msg) #=> [['labels', '~foo ~"bar baz"']]
# msg #=> "hello\nworld"
# ```
- def extract_commands(content)
+ def extract_commands(content, only: nil)
return [content, []] unless content
content = content.dup
@@ -37,7 +37,7 @@ module Gitlab
commands = []
content.delete!("\r")
- content.gsub!(commands_regex) do
+ content.gsub!(commands_regex(only: only)) do
if $~[:cmd]
commands << [$~[:cmd].downcase, $~[:arg]].reject(&:blank?)
''
@@ -60,8 +60,8 @@ module Gitlab
# It looks something like:
#
# /^\/(?<cmd>close|reopen|...)(?:( |$))(?<arg>[^\/\n]*)(?:\n|$)/
- def commands_regex
- names = command_names.map(&:to_s)
+ def commands_regex(only:)
+ names = command_names(limit_to_commands: only).map(&:to_s)
@commands_regex ||= %r{
(?<code>
@@ -133,10 +133,14 @@ module Gitlab
[content, commands]
end
- def command_names
+ def command_names(limit_to_commands:)
command_definitions.flat_map do |command|
next if command.noop?
+ if limit_to_commands && (command.all_names & limit_to_commands).empty?
+ next
+ end
+
command.all_names
end.compact
end
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index 5ce3eda2ccb..458737f31eb 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -4,8 +4,10 @@ module Gitlab
class SearchResults
class FoundBlob
include EncodingHelper
+ include Presentable
+ include BlobLanguageFromGitAttributes
- attr_reader :id, :filename, :basename, :ref, :startline, :data, :project_id
+ attr_reader :id, :filename, :basename, :ref, :startline, :data, :project
def initialize(opts = {})
@id = opts.fetch(:id, nil)
@@ -15,6 +17,11 @@ module Gitlab
@startline = opts.fetch(:startline, nil)
@data = encode_utf8(opts.fetch(:data, nil))
@per_page = opts.fetch(:per_page, 20)
+ @project = opts.fetch(:project, nil)
+ # Some caller does not have project object (e.g. elastic search),
+ # yet they can trigger many calls in one go,
+ # causing duplicated queries.
+ # Allow those to just pass project_id instead.
@project_id = opts.fetch(:project_id, nil)
end
@@ -22,8 +29,12 @@ module Gitlab
filename
end
- def no_highlighting?
- false
+ def project_id
+ @project_id || @project&.id
+ end
+
+ def present
+ super(presenter_class: BlobPresenter)
end
end
diff --git a/lib/gitlab/sentry.rb b/lib/gitlab/sentry.rb
index 24e3866128b..8079c5882c4 100644
--- a/lib/gitlab/sentry.rb
+++ b/lib/gitlab/sentry.rb
@@ -7,7 +7,7 @@ module Gitlab
end
def self.context(current_user = nil)
- return unless self.enabled?
+ return unless enabled?
Raven.tags_context(locale: I18n.locale)
@@ -29,14 +29,22 @@ module Gitlab
#
# Provide an issue URL for follow up.
def self.track_exception(exception, issue_url: nil, extra: {})
+ track_acceptable_exception(exception, issue_url: issue_url, extra: extra)
+
+ raise exception if should_raise?
+ end
+
+ # This should be used when you do not want to raise an exception in
+ # development and test. If you need development and test to behave
+ # just the same as production you can use this instead of
+ # track_exception.
+ def self.track_acceptable_exception(exception, issue_url: nil, extra: {})
if enabled?
extra[:issue_url] = issue_url if issue_url
context # Make sure we've set everything we know in the context
Raven.capture_exception(exception, extra: extra)
end
-
- raise exception if should_raise?
end
def self.program_context
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index 16c1edb2f11..c6a6fb9b5ce 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -108,23 +108,6 @@ module Gitlab
success
end
- # Fetch remote for repository
- #
- # repository - an instance of Git::Repository
- # remote - remote name
- # ssh_auth - SSH known_hosts data and a private key to use for public-key authentication
- # forced - should we use --force flag?
- # no_tags - should we use --no-tags flag?
- #
- # Ex.
- # fetch_remote(my_repo, "upstream")
- #
- def fetch_remote(repository, remote, ssh_auth: nil, forced: false, no_tags: false, prune: true)
- wrapped_gitaly_errors do
- repository.gitaly_repository_client.fetch_remote(remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags, timeout: git_timeout, prune: prune)
- end
- end
-
# Move repository reroutes to mv_directory which is an alias for
# mv_namespace. Given the underlying implementation is a move action,
# indescriminate of what the folders might be.
diff --git a/lib/gitlab/slash_commands/issue_new.rb b/lib/gitlab/slash_commands/issue_new.rb
index 25f965e843d..6396b828dc7 100644
--- a/lib/gitlab/slash_commands/issue_new.rb
+++ b/lib/gitlab/slash_commands/issue_new.rb
@@ -3,7 +3,7 @@ module Gitlab
class IssueNew < IssueCommand
def self.match(text)
# we can not match \n with the dot by passing the m modifier as than
- # the title and description are not seperated
+ # the title and description are not separated
/\Aissue\s+(new|create)\s+(?<title>[^\n]*)\n*(?<description>(.|\n)*)/.match(text)
end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index cc0817bdcd2..069cd1f802a 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -62,6 +62,7 @@ module Gitlab
clusters_applications_ingress: count(::Clusters::Applications::Ingress.installed),
clusters_applications_prometheus: count(::Clusters::Applications::Prometheus.installed),
clusters_applications_runner: count(::Clusters::Applications::Runner.installed),
+ clusters_applications_knative: count(::Clusters::Applications::Knative.installed),
in_review_folder: count(::Environment.in_review_folder),
groups: count(Group),
issues: count(Issue),
@@ -126,7 +127,6 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def services_usage
types = {
- JiraService: :projects_jira_active,
SlackService: :projects_slack_notifications_active,
SlackSlashCommandsService: :projects_slack_slash_active,
PrometheusService: :projects_prometheus_active
@@ -134,6 +134,23 @@ module Gitlab
results = count(Service.unscoped.where(type: types.keys, active: true).group(:type), fallback: Hash.new(-1))
types.each_with_object({}) { |(klass, key), response| response[key] = results[klass.to_s] || 0 }
+ .merge(jira_usage)
+ end
+
+ def jira_usage
+ # Jira Cloud does not support custom domains as per https://jira.atlassian.com/browse/CLOUD-6999
+ # so we can just check for subdomains of atlassian.net
+ services = count(
+ Service.unscoped.where(type: :JiraService, active: true)
+ .group("CASE WHEN properties LIKE '%.atlassian.net%' THEN 'cloud' ELSE 'server' END"),
+ fallback: Hash.new(-1)
+ )
+
+ {
+ projects_jira_server_active: services['server'] || 0,
+ projects_jira_cloud_active: services['cloud'] || 0,
+ projects_jira_active: services['server'] == -1 ? -1 : services.values.sum
+ }
end
def count(relation, fallback: -1)
diff --git a/lib/gitlab/user_extractor.rb b/lib/gitlab/user_extractor.rb
index bd0d24e4369..874599688bb 100644
--- a/lib/gitlab/user_extractor.rb
+++ b/lib/gitlab/user_extractor.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
# This class extracts all users found in a piece of text by the username or the
-# email adress
+# email address
module Gitlab
class UserExtractor
@@ -14,13 +14,11 @@ module Gitlab
@text = text
end
- # rubocop: disable CodeReuse/ActiveRecord
def users
return User.none unless @text.present?
@users ||= User.from_union(union_relations)
end
- # rubocop: enable CodeReuse/ActiveRecord
def usernames
matches[:usernames]
diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb
index 2c92458f777..9e59137a2c0 100644
--- a/lib/gitlab/utils.rb
+++ b/lib/gitlab/utils.rb
@@ -16,6 +16,11 @@ module Gitlab
str.force_encoding(Encoding::UTF_8)
end
+ # Append path to host, making sure there's one single / in between
+ def append_path(host, path)
+ "#{host.to_s.sub(%r{\/+$}, '')}/#{path.to_s.sub(%r{^\/+}, '')}"
+ end
+
# A slugified version of the string, suitable for inclusion in URLs and
# domain names. Rules:
#
diff --git a/lib/google_api/auth.rb b/lib/google_api/auth.rb
index e724e58e9ca..56f056fd869 100644
--- a/lib/google_api/auth.rb
+++ b/lib/google_api/auth.rb
@@ -16,7 +16,7 @@ module GoogleApi
client.auth_code.authorize_url(
redirect_uri: redirect_uri,
scope: scope,
- state: state # This is used for arbitary redirection
+ state: state # This is used for arbitrary redirection
)
end
diff --git a/lib/json_web_token/hmac_token.rb b/lib/json_web_token/hmac_token.rb
new file mode 100644
index 00000000000..ceb1b9c913f
--- /dev/null
+++ b/lib/json_web_token/hmac_token.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'jwt'
+
+module JSONWebToken
+ class HMACToken < Token
+ IAT_LEEWAY = 60
+ JWT_ALGORITHM = 'HS256'
+
+ def initialize(secret)
+ super()
+
+ @secret = secret
+ end
+
+ def self.decode(token, secret, leeway: IAT_LEEWAY, verify_iat: true)
+ JWT.decode(token, secret, true, leeway: leeway, verify_iat: verify_iat, algorithm: JWT_ALGORITHM)
+ end
+
+ def encoded
+ JWT.encode(payload, secret, JWT_ALGORITHM)
+ end
+
+ private
+
+ attr_reader :secret
+ end
+end
diff --git a/lib/json_web_token/token.rb b/lib/json_web_token/token.rb
index ce5d6f248d0..c59beef02c9 100644
--- a/lib/json_web_token/token.rb
+++ b/lib/json_web_token/token.rb
@@ -1,17 +1,22 @@
# frozen_string_literal: true
+require 'securerandom'
+
module JSONWebToken
class Token
attr_accessor :issuer, :subject, :audience, :id
attr_accessor :issued_at, :not_before, :expire_time
+ DEFAULT_NOT_BEFORE_TIME = 5
+ DEFAULT_EXPIRE_TIME = 60
+
def initialize
@id = SecureRandom.uuid
@issued_at = Time.now
# we give a few seconds for time shift
- @not_before = issued_at - 5.seconds
+ @not_before = issued_at - DEFAULT_NOT_BEFORE_TIME
# default 60 seconds should be more than enough for this authentication token
- @expire_time = issued_at + 1.minute
+ @expire_time = issued_at + DEFAULT_EXPIRE_TIME
@custom_payload = {}
end
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index e5b5f3548e4..a2c3e32948f 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -1,6 +1,7 @@
namespace :gitlab do
desc 'GitLab | Check the configuration of GitLab and its environment'
task check: %w{gitlab:gitlab_shell:check
+ gitlab:gitaly:check
gitlab:sidekiq:check
gitlab:incoming_email:check
gitlab:ldap:check
@@ -44,13 +45,6 @@ namespace :gitlab do
start_checking "GitLab Shell"
check_gitlab_shell
- Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- check_repo_base_exists
- check_repo_base_is_not_symlink
- check_repo_base_user_and_group
- check_repo_base_permissions
- check_repos_hooks_directory_is_link
- end
check_gitlab_shell_self_test
finished_checking "GitLab Shell"
@@ -59,152 +53,6 @@ namespace :gitlab do
# Checks
########################
- def check_repo_base_exists
- puts "Repo base directory exists?"
-
- Gitlab.config.repositories.storages.each do |name, repository_storage|
- repo_base_path = repository_storage.legacy_disk_path
- print "#{name}... "
-
- if File.exist?(repo_base_path)
- puts "yes".color(:green)
- else
- puts "no".color(:red)
- puts "#{repo_base_path} is missing".color(:red)
- try_fixing_it(
- "This should have been created when setting up GitLab Shell.",
- "Make sure it's set correctly in config/gitlab.yml",
- "Make sure GitLab Shell is installed correctly."
- )
- for_more_information(
- see_installation_guide_section "GitLab Shell"
- )
- fix_and_rerun
- end
- end
- end
-
- def check_repo_base_is_not_symlink
- puts "Repo storage directories are symlinks?"
-
- Gitlab.config.repositories.storages.each do |name, repository_storage|
- repo_base_path = repository_storage.legacy_disk_path
- print "#{name}... "
-
- unless File.exist?(repo_base_path)
- puts "can't check because of previous errors".color(:magenta)
- break
- end
-
- unless File.symlink?(repo_base_path)
- puts "no".color(:green)
- else
- puts "yes".color(:red)
- try_fixing_it(
- "Make sure it's set to the real directory in config/gitlab.yml"
- )
- fix_and_rerun
- end
- end
- end
-
- def check_repo_base_permissions
- puts "Repo paths access is drwxrws---?"
-
- Gitlab.config.repositories.storages.each do |name, repository_storage|
- repo_base_path = repository_storage.legacy_disk_path
- print "#{name}... "
-
- unless File.exist?(repo_base_path)
- puts "can't check because of previous errors".color(:magenta)
- break
- end
-
- if File.stat(repo_base_path).mode.to_s(8).ends_with?("2770")
- puts "yes".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- "sudo chmod -R ug+rwX,o-rwx #{repo_base_path}",
- "sudo chmod -R ug-s #{repo_base_path}",
- "sudo find #{repo_base_path} -type d -print0 | sudo xargs -0 chmod g+s"
- )
- for_more_information(
- see_installation_guide_section "GitLab Shell"
- )
- fix_and_rerun
- end
- end
- end
-
- def check_repo_base_user_and_group
- gitlab_shell_ssh_user = Gitlab.config.gitlab_shell.ssh_user
- puts "Repo paths owned by #{gitlab_shell_ssh_user}:root, or #{gitlab_shell_ssh_user}:#{Gitlab.config.gitlab_shell.owner_group}?"
-
- Gitlab.config.repositories.storages.each do |name, repository_storage|
- repo_base_path = repository_storage.legacy_disk_path
- print "#{name}... "
-
- unless File.exist?(repo_base_path)
- puts "can't check because of previous errors".color(:magenta)
- break
- end
-
- user_id = uid_for(gitlab_shell_ssh_user)
- root_group_id = gid_for('root')
- group_ids = [root_group_id, gid_for(Gitlab.config.gitlab_shell.owner_group)]
- if File.stat(repo_base_path).uid == user_id && group_ids.include?(File.stat(repo_base_path).gid)
- puts "yes".color(:green)
- else
- puts "no".color(:red)
- puts " User id for #{gitlab_shell_ssh_user}: #{user_id}. Groupd id for root: #{root_group_id}".color(:blue)
- try_fixing_it(
- "sudo chown -R #{gitlab_shell_ssh_user}:root #{repo_base_path}"
- )
- for_more_information(
- see_installation_guide_section "GitLab Shell"
- )
- fix_and_rerun
- end
- end
- end
-
- def check_repos_hooks_directory_is_link
- print "hooks directories in repos are links: ... "
-
- gitlab_shell_hooks_path = Gitlab.config.gitlab_shell.hooks_path
-
- unless Project.count > 0
- puts "can't check, you have no projects".color(:magenta)
- return
- end
-
- puts ""
-
- Project.find_each(batch_size: 100) do |project|
- print sanitized_message(project)
- project_hook_directory = File.join(project.repository.path_to_repo, "hooks")
-
- if project.empty_repo?
- puts "repository is empty".color(:magenta)
- elsif File.directory?(project_hook_directory) && File.directory?(gitlab_shell_hooks_path) &&
- (File.realpath(project_hook_directory) == File.realpath(gitlab_shell_hooks_path))
- puts 'ok'.color(:green)
- else
- puts "wrong or missing hooks".color(:red)
- try_fixing_it(
- sudo_gitlab("#{File.join(gitlab_shell_path, 'bin/create-hooks')} #{repository_storage_paths_args.join(' ')}"),
- 'Check the hooks_path in config/gitlab.yml',
- 'Check your gitlab-shell installation'
- )
- for_more_information(
- see_installation_guide_section "GitLab Shell"
- )
- fix_and_rerun
- end
- end
- end
-
def check_gitlab_shell_self_test
gitlab_shell_repo_base = gitlab_shell_path
check_cmd = File.expand_path('bin/check', gitlab_shell_repo_base)
@@ -247,6 +95,26 @@ namespace :gitlab do
end
end
+ namespace :gitaly do
+ desc 'GitLab | Check the health of Gitaly'
+ task check: :gitlab_environment do
+ warn_user_is_not_gitlab
+ start_checking 'Gitaly'
+
+ Gitlab::HealthChecks::GitalyCheck.readiness.each do |result|
+ print "#{result.labels[:shard]} ... "
+
+ if result.success
+ puts 'OK'.color(:green)
+ else
+ puts "FAIL: #{result.message}".color(:red)
+ end
+ end
+
+ finished_checking 'Gitaly'
+ end
+ end
+
namespace :sidekiq do
desc "GitLab | Check the configuration of Sidekiq"
task check: :gitlab_environment do
diff --git a/locale/ar_SA/gitlab.po b/locale/ar_SA/gitlab.po
index 903d72d9ea8..5a94d7fc39d 100644
--- a/locale/ar_SA/gitlab.po
+++ b/locale/ar_SA/gitlab.po
@@ -2913,7 +2913,7 @@ msgstr ""
msgid "Elasticsearch"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Elasticsearch integration. Elasticsearch AWS IAM."
msgstr ""
msgid "Email"
diff --git a/locale/bg/gitlab.po b/locale/bg/gitlab.po
index e3a5e191023..dd911bd5639 100644
--- a/locale/bg/gitlab.po
+++ b/locale/bg/gitlab.po
@@ -2777,7 +2777,7 @@ msgstr ""
msgid "Elasticsearch"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Elasticsearch integration. Elasticsearch AWS IAM."
msgstr ""
msgid "Email"
diff --git a/locale/ca_ES/gitlab.po b/locale/ca_ES/gitlab.po
index 91d5fb17000..30c99c93d6c 100644
--- a/locale/ca_ES/gitlab.po
+++ b/locale/ca_ES/gitlab.po
@@ -2777,7 +2777,7 @@ msgstr ""
msgid "Elasticsearch"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Elasticsearch integration. Elasticsearch AWS IAM."
msgstr ""
msgid "Email"
diff --git a/locale/cs_CZ/gitlab.po b/locale/cs_CZ/gitlab.po
index 867b9b27ad2..59f6687d75e 100644
--- a/locale/cs_CZ/gitlab.po
+++ b/locale/cs_CZ/gitlab.po
@@ -2845,7 +2845,7 @@ msgstr ""
msgid "Elasticsearch"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Elasticsearch integration. Elasticsearch AWS IAM."
msgstr ""
msgid "Email"
diff --git a/locale/da_DK/gitlab.po b/locale/da_DK/gitlab.po
index f8412f28cbe..0488747cca1 100644
--- a/locale/da_DK/gitlab.po
+++ b/locale/da_DK/gitlab.po
@@ -2777,7 +2777,7 @@ msgstr ""
msgid "Elasticsearch"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Elasticsearch integration. Elasticsearch AWS IAM."
msgstr ""
msgid "Email"
diff --git a/locale/de/gitlab.po b/locale/de/gitlab.po
index 5f9fbc1cc39..40f9365ef7d 100644
--- a/locale/de/gitlab.po
+++ b/locale/de/gitlab.po
@@ -2777,7 +2777,7 @@ msgstr ""
msgid "Elasticsearch"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Elasticsearch integration. Elasticsearch AWS IAM."
msgstr ""
msgid "Email"
diff --git a/locale/eo/gitlab.po b/locale/eo/gitlab.po
index 15d537ff3b4..3723844cf84 100644
--- a/locale/eo/gitlab.po
+++ b/locale/eo/gitlab.po
@@ -2777,7 +2777,7 @@ msgstr ""
msgid "Elasticsearch"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Elasticsearch integration. Elasticsearch AWS IAM."
msgstr ""
msgid "Email"
diff --git a/locale/es/gitlab.po b/locale/es/gitlab.po
index 0d8ee05b364..3e85f7438a4 100644
--- a/locale/es/gitlab.po
+++ b/locale/es/gitlab.po
@@ -2777,7 +2777,7 @@ msgstr ""
msgid "Elasticsearch"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Elasticsearch integration. Elasticsearch AWS IAM."
msgstr ""
msgid "Email"
diff --git a/locale/et_EE/gitlab.po b/locale/et_EE/gitlab.po
index 1f699a8e06d..4d1cdfd98b4 100644
--- a/locale/et_EE/gitlab.po
+++ b/locale/et_EE/gitlab.po
@@ -2777,7 +2777,7 @@ msgstr ""
msgid "Elasticsearch"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Elasticsearch integration. Elasticsearch AWS IAM."
msgstr ""
msgid "Email"
diff --git a/locale/fil_PH/gitlab.po b/locale/fil_PH/gitlab.po
index c9b371f8df3..547b7859926 100644
--- a/locale/fil_PH/gitlab.po
+++ b/locale/fil_PH/gitlab.po
@@ -2777,7 +2777,7 @@ msgstr ""
msgid "Elasticsearch"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Elasticsearch integration. Elasticsearch AWS IAM."
msgstr ""
msgid "Email"
diff --git a/locale/fr/gitlab.po b/locale/fr/gitlab.po
index 1925235d48a..ac509d27241 100644
--- a/locale/fr/gitlab.po
+++ b/locale/fr/gitlab.po
@@ -2777,7 +2777,7 @@ msgstr "Modifier l’identité de %{user_name}"
msgid "Elasticsearch"
msgstr "Elasticsearch"
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Elasticsearch integration. Elasticsearch AWS IAM."
msgstr "Intégration d’Elasticsearch. AWS Elasticsearch IAM."
msgid "Email"
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 23afc6b245b..d3e1a51370e 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -103,6 +103,9 @@ msgstr ""
msgid "%{counter_storage} (%{counter_repositories} repositories, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS)"
msgstr ""
+msgid "%{count} more assignees"
+msgstr ""
+
msgid "%{count} participant"
msgid_plural "%{count} participants"
msgstr[0] ""
@@ -264,6 +267,9 @@ msgstr ""
msgid "A deleted user"
msgstr ""
+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."
msgstr ""
@@ -336,6 +342,9 @@ msgstr ""
msgid "Add a table"
msgstr ""
+msgid "Add image comment"
+msgstr ""
+
msgid "Add license"
msgstr ""
@@ -1346,12 +1355,24 @@ msgstr ""
msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which may incur additional costs depending on the hosting provider your Kubernetes cluster is installed on. If you are using Google Kubernetes Engine, you can %{pricingLink}."
msgstr ""
+msgid "ClusterIntegration|A Knative build extends Kubernetes and utilizes existing Kubernetes primitives to provide you with the ability to run on-cluster container builds from source. For example, you can write a build that uses Kubernetes-native resources to obtain your source code from a repository, build it into container a image, and then run that image."
+msgstr ""
+
msgid "ClusterIntegration|API URL"
msgstr ""
msgid "ClusterIntegration|Add Kubernetes cluster"
msgstr ""
+msgid "ClusterIntegration|Add a Kubernetes cluster integration"
+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 an integration to your group will share the cluster across all your projects."
+msgstr ""
+
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
msgstr ""
@@ -1454,6 +1475,9 @@ msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine project"
msgstr ""
+msgid "ClusterIntegration|Group cluster"
+msgstr ""
+
msgid "ClusterIntegration|Helm Tiller"
msgstr ""
@@ -1496,13 +1520,16 @@ msgstr ""
msgid "ClusterIntegration|JupyterHub, a multi-user Hub, spawns, manages, and proxies multiple instances of the single-user Jupyter notebook server. JupyterHub can be used to serve notebooks to a class of students, a corporate data science group, or a scientific research group."
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster"
+msgid "ClusterIntegration|Knative"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster details"
+msgid "ClusterIntegration|Knative Domain Name:"
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster integration"
+msgid "ClusterIntegration|Kubernetes cluster details"
msgstr ""
msgid "ClusterIntegration|Kubernetes cluster is being created on Google Kubernetes Engine..."
@@ -1514,7 +1541,7 @@ msgstr ""
msgid "ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine. Refresh the page to see Kubernetes cluster's details"
msgstr ""
-msgid "ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}"
+msgid "ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
msgid "ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project"
@@ -1523,10 +1550,13 @@ msgstr ""
msgid "ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}."
msgstr ""
-msgid "ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}."
+msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
msgstr ""
-msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
+msgid "ClusterIntegration|Learn more about Kubernetes"
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about group Kubernetes clusters"
msgstr ""
msgid "ClusterIntegration|Machine type"
@@ -1571,6 +1601,9 @@ msgstr ""
msgid "ClusterIntegration|Point a wildcard DNS to this generated IP address in order to access your application after it has been deployed."
msgstr ""
+msgid "ClusterIntegration|Project cluster"
+msgstr ""
+
msgid "ClusterIntegration|Project namespace"
msgstr ""
@@ -1583,7 +1616,7 @@ msgstr ""
msgid "ClusterIntegration|Prometheus is an open-source monitoring system with %{gitlabIntegrationLink} to monitor deployed applications."
msgstr ""
-msgid "ClusterIntegration|RBAC-enabled cluster (experimental)"
+msgid "ClusterIntegration|RBAC-enabled cluster"
msgstr ""
msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration."
@@ -1655,18 +1688,12 @@ msgstr ""
msgid "ClusterIntegration|The IP address 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 ""
-msgid "ClusterIntegration|The default cluster configuration grants access to many functionalities needed to successfully build and deploy a containerised application."
-msgstr ""
-
msgid "ClusterIntegration|This account must have permissions to create a Kubernetes cluster in the %{link_to_container_project} specified below"
msgstr ""
msgid "ClusterIntegration|This option will allow you to install applications on RBAC clusters."
msgstr ""
-msgid "ClusterIntegration|Toggle Kubernetes Cluster"
-msgstr ""
-
msgid "ClusterIntegration|Toggle Kubernetes cluster"
msgstr ""
@@ -1721,12 +1748,18 @@ msgstr ""
msgid "Collapse sidebar"
msgstr ""
+msgid "Comment"
+msgstr ""
+
msgid "Comment & resolve discussion"
msgstr ""
msgid "Comment & unresolve discussion"
msgstr ""
+msgid "Comment form position"
+msgstr ""
+
msgid "Comments"
msgstr ""
@@ -1974,6 +2007,9 @@ msgstr ""
msgid "Copy file path to clipboard"
msgstr ""
+msgid "Copy link"
+msgstr ""
+
msgid "Copy reference to clipboard"
msgstr ""
@@ -1986,6 +2022,9 @@ msgstr ""
msgid "Copy token to clipboard"
msgstr ""
+msgid "Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation.%{linkEnd}"
+msgstr ""
+
msgid "Create"
msgstr ""
@@ -2094,6 +2133,9 @@ msgstr ""
msgid "Custom CI config path"
msgstr ""
+msgid "Custom hostname (for private commit emails)"
+msgstr ""
+
msgid "Custom notification events"
msgstr ""
@@ -2169,7 +2211,7 @@ msgstr ""
msgid "Define a custom pattern with cron syntax"
msgstr ""
-msgid "DelayedJobs|Are you sure you want to run %{jobName} immediately? This job will run automatically after it's timer finishes."
+msgid "DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after it's timer finishes."
msgstr ""
msgid "DelayedJobs|Are you sure you want to run %{job_name} immediately? This job will run automatically after it's timer finishes."
@@ -2190,6 +2232,9 @@ msgstr ""
msgid "Delete Snippet"
msgstr ""
+msgid "Delete comment"
+msgstr ""
+
msgid "Delete list"
msgstr ""
@@ -2726,6 +2771,9 @@ msgstr ""
msgid "Expiration date"
msgstr ""
+msgid "Explain the problem. If appropriate, provide a link to the relevant issue or comment."
+msgstr ""
+
msgid "Explore"
msgstr ""
@@ -3014,9 +3062,15 @@ msgstr ""
msgid "Group Runners"
msgstr ""
+msgid "Group URL"
+msgstr ""
+
msgid "Group avatar"
msgstr ""
+msgid "Group description"
+msgstr ""
+
msgid "Group description (optional)"
msgstr ""
@@ -3549,6 +3603,9 @@ msgstr ""
msgid "Last edited by %{name}"
msgstr ""
+msgid "Last reply by"
+msgstr ""
+
msgid "Last update"
msgstr ""
@@ -4150,12 +4207,18 @@ msgstr ""
msgid "Notes|Are you sure you want to cancel creating this comment?"
msgstr ""
+msgid "Notes|Collapse replies"
+msgstr ""
+
msgid "Notes|Show all activity"
msgstr ""
msgid "Notes|Show comments only"
msgstr ""
+msgid "Notes|Show history only"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -4488,15 +4551,24 @@ msgstr ""
msgid "Pipelines|This project is not currently set up to run pipelines."
msgstr ""
+msgid "Pipeline|Commit"
+msgstr ""
+
msgid "Pipeline|Create for"
msgstr ""
msgid "Pipeline|Create pipeline"
msgstr ""
+msgid "Pipeline|Duration"
+msgstr ""
+
msgid "Pipeline|Existing branch name or tag"
msgstr ""
+msgid "Pipeline|Pipeline"
+msgstr ""
+
msgid "Pipeline|Run Pipeline"
msgstr ""
@@ -4506,6 +4578,12 @@ msgstr ""
msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default."
msgstr ""
+msgid "Pipeline|Stages"
+msgstr ""
+
+msgid "Pipeline|Status"
+msgstr ""
+
msgid "Pipeline|Stop pipeline"
msgstr ""
@@ -4542,12 +4620,18 @@ msgstr ""
msgid "Please accept the Terms of Service before continuing."
msgstr ""
+msgid "Please choose a group URL with no special characters."
+msgstr ""
+
msgid "Please convert them to %{link_to_git}, and go through the %{link_to_import_flow} again."
msgstr ""
msgid "Please convert them to Git on Google Code, and go through the %{link_to_import_flow} again."
msgstr ""
+msgid "Please fill in a descriptive name for your group."
+msgstr ""
+
msgid "Please note that this application is not provided by GitLab and you should verify its authenticity before allowing access."
msgstr ""
@@ -4560,6 +4644,9 @@ msgstr ""
msgid "Please try again"
msgstr ""
+msgid "Please use this form to report users to GitLab who create spam issues, comments or behave inappropriately."
+msgstr ""
+
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
@@ -4611,9 +4698,6 @@ msgstr ""
msgid "Profiles| You are going to change the username %{currentUsernameBold} to %{newUsernameBold}. Profile and projects will be redirected to the %{newUsername} namespace but this redirect will expire once the %{currentUsername} namespace is registered by another user or group. Please update your Git repository remotes as soon as possible."
msgstr ""
-msgid "Profiles|%{author_name} made a private contribution"
-msgstr ""
-
msgid "Profiles|Account scheduled for removal."
msgstr ""
@@ -4674,6 +4758,12 @@ msgstr ""
msgid "Profiles|Invalid username"
msgstr ""
+msgid "Profiles|Learn more"
+msgstr ""
+
+msgid "Profiles|Made a private contribution"
+msgstr ""
+
msgid "Profiles|Main settings"
msgstr ""
@@ -4713,6 +4803,9 @@ msgstr ""
msgid "Profiles|This email will be displayed on your public profile."
msgstr ""
+msgid "Profiles|This email will be used for web based operations, such as edits and merges. %{learn_more}"
+msgstr ""
+
msgid "Profiles|This emoji and message will appear on your profile and throughout the interface."
msgstr ""
@@ -4737,6 +4830,9 @@ msgstr ""
msgid "Profiles|Upload new avatar"
msgstr ""
+msgid "Profiles|Use a private email - %{email}"
+msgstr ""
+
msgid "Profiles|Username change failed - %{message}"
msgstr ""
@@ -4899,6 +4995,9 @@ msgstr ""
msgid "Projects shared with %{group_name}"
msgstr ""
+msgid "Projects that belong to a group are prefixed with the group namespace. Existing projects may be moved into a group."
+msgstr ""
+
msgid "ProjectsDropdown|Frequently visited"
msgstr ""
@@ -5126,6 +5225,9 @@ msgstr ""
msgid "Reply to this email directly or %{view_it_on_gitlab}."
msgstr ""
+msgid "Report abuse to GitLab"
+msgstr ""
+
msgid "Reporting"
msgstr ""
@@ -5814,6 +5916,9 @@ msgstr ""
msgid "Status"
msgstr ""
+msgid "Stop environment"
+msgstr ""
+
msgid "Stop impersonation"
msgstr ""
@@ -5823,6 +5928,9 @@ msgstr ""
msgid "Stopped"
msgstr ""
+msgid "Stopping this environment is currently not possible as a deployment is in progress"
+msgstr ""
+
msgid "Storage"
msgstr ""
@@ -6149,7 +6257,7 @@ msgstr ""
msgid "This is a confidential issue."
msgstr ""
-msgid "This is a delayed to run in "
+msgid "This is a delayed job to run in %{remainingTime}"
msgstr ""
msgid "This is the author's first Merge Request to this project."
@@ -6191,6 +6299,9 @@ msgstr ""
msgid "This job is an out-of-date deployment to %{environmentLink}. View the most recent deployment %{deploymentLink}."
msgstr ""
+msgid "This job is archived. Only the complete pipeline can be retried."
+msgstr ""
+
msgid "This job is creating a deployment to %{environmentLink} and will overwrite the %{deploymentLink}."
msgstr ""
@@ -6254,6 +6365,9 @@ msgstr ""
msgid "This setting can be overridden in each project."
msgstr ""
+msgid "This setting will update the hostname that is used to generate private commit emails. %{learn_more}"
+msgstr ""
+
msgid "This source diff could not be displayed because it is too large."
msgstr ""
@@ -6278,6 +6392,9 @@ msgstr ""
msgid "Time between merge request creation and merge/close"
msgstr ""
+msgid "Time estimate"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -6492,6 +6609,9 @@ msgstr ""
msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
msgstr ""
+msgid "Today"
+msgstr ""
+
msgid "Todo"
msgstr ""
@@ -6525,6 +6645,9 @@ msgstr ""
msgid "Token"
msgstr ""
+msgid "Tomorrow"
+msgstr ""
+
msgid "Too many changes to show."
msgstr ""
@@ -6789,6 +6912,9 @@ msgstr ""
msgid "View replaced file @ "
msgstr ""
+msgid "View the documentation"
+msgstr ""
+
msgid "Visibility and access controls"
msgstr ""
@@ -6840,6 +6966,9 @@ msgstr ""
msgid "Who can see this group?"
msgstr ""
+msgid "Who will be able to see this group?"
+msgstr ""
+
msgid "Wiki"
msgstr ""
@@ -6987,6 +7116,9 @@ msgstr ""
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
+msgid "Yesterday"
+msgstr ""
+
msgid "You are an admin, which means granting access to <strong>%{client_name}</strong> will allow them to interact with GitLab as an admin as well. Proceed with caution."
msgstr ""
@@ -7454,6 +7586,11 @@ msgstr ""
msgid "remove due date"
msgstr ""
+msgid "reply"
+msgid_plural "replies"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "source"
msgstr ""
diff --git a/locale/gl_ES/gitlab.po b/locale/gl_ES/gitlab.po
index 44c47901b56..1b899318067 100644
--- a/locale/gl_ES/gitlab.po
+++ b/locale/gl_ES/gitlab.po
@@ -2777,7 +2777,7 @@ msgstr ""
msgid "Elasticsearch"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Elasticsearch integration. Elasticsearch AWS IAM."
msgstr ""
msgid "Email"
diff --git a/locale/he_IL/gitlab.po b/locale/he_IL/gitlab.po
index 7cff1c5d712..78b1d69f02f 100644
--- a/locale/he_IL/gitlab.po
+++ b/locale/he_IL/gitlab.po
@@ -2845,7 +2845,7 @@ msgstr ""
msgid "Elasticsearch"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Elasticsearch integration. Elasticsearch AWS IAM."
msgstr ""
msgid "Email"
diff --git a/locale/id_ID/gitlab.po b/locale/id_ID/gitlab.po
index 2f3c35e4253..54fd6c61821 100644
--- a/locale/id_ID/gitlab.po
+++ b/locale/id_ID/gitlab.po
@@ -2743,7 +2743,7 @@ msgstr ""
msgid "Elasticsearch"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Elasticsearch integration. Elasticsearch AWS IAM."
msgstr ""
msgid "Email"
diff --git a/locale/it/gitlab.po b/locale/it/gitlab.po
index 984a200c958..bccbd3d1f13 100644
--- a/locale/it/gitlab.po
+++ b/locale/it/gitlab.po
@@ -2777,7 +2777,7 @@ msgstr ""
msgid "Elasticsearch"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Elasticsearch integration. Elasticsearch AWS IAM."
msgstr ""
msgid "Email"
diff --git a/locale/ja/gitlab.po b/locale/ja/gitlab.po
index ac940230ac2..d9530519e9c 100644
--- a/locale/ja/gitlab.po
+++ b/locale/ja/gitlab.po
@@ -2743,7 +2743,7 @@ msgstr "%{user_name} の ID を編集する"
msgid "Elasticsearch"
msgstr "Elasticsearch"
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Elasticsearch integration. Elasticsearch AWS IAM."
msgstr "Elasticsearch の統合。Elasticsearch AWS IAM。"
msgid "Email"
diff --git a/locale/ko/gitlab.po b/locale/ko/gitlab.po
index a50f5bfacef..82a770db549 100644
--- a/locale/ko/gitlab.po
+++ b/locale/ko/gitlab.po
@@ -2743,7 +2743,7 @@ msgstr ""
msgid "Elasticsearch"
msgstr "Elasticsearch"
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Elasticsearch integration. Elasticsearch AWS IAM."
msgstr ""
msgid "Email"
diff --git a/locale/mn_MN/gitlab.po b/locale/mn_MN/gitlab.po
index c256a2d2b1f..29b50db6d9d 100644
--- a/locale/mn_MN/gitlab.po
+++ b/locale/mn_MN/gitlab.po
@@ -2777,7 +2777,7 @@ msgstr ""
msgid "Elasticsearch"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Elasticsearch integration. Elasticsearch AWS IAM."
msgstr ""
msgid "Email"
diff --git a/locale/nb_NO/gitlab.po b/locale/nb_NO/gitlab.po
index 9be8a4e8d0b..e53838f371e 100644
--- a/locale/nb_NO/gitlab.po
+++ b/locale/nb_NO/gitlab.po
@@ -2777,7 +2777,7 @@ msgstr ""
msgid "Elasticsearch"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Elasticsearch integration. Elasticsearch AWS IAM."
msgstr ""
msgid "Email"
diff --git a/locale/nl_NL/gitlab.po b/locale/nl_NL/gitlab.po
index 2ad2eaafc33..1f654724709 100644
--- a/locale/nl_NL/gitlab.po
+++ b/locale/nl_NL/gitlab.po
@@ -2777,7 +2777,7 @@ msgstr ""
msgid "Elasticsearch"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Elasticsearch integration. Elasticsearch AWS IAM."
msgstr ""
msgid "Email"
diff --git a/locale/pl_PL/gitlab.po b/locale/pl_PL/gitlab.po
index a4bd1fe747b..d3e954a2ab1 100644
--- a/locale/pl_PL/gitlab.po
+++ b/locale/pl_PL/gitlab.po
@@ -2845,7 +2845,7 @@ msgstr ""
msgid "Elasticsearch"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Elasticsearch integration. Elasticsearch AWS IAM."
msgstr ""
msgid "Email"
diff --git a/locale/pt_BR/gitlab.po b/locale/pt_BR/gitlab.po
index 79e365319c9..ae4e6df4902 100644
--- a/locale/pt_BR/gitlab.po
+++ b/locale/pt_BR/gitlab.po
@@ -2777,7 +2777,7 @@ msgstr "Editar identidade para %{user_name}"
msgid "Elasticsearch"
msgstr "Elasticsearch"
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Elasticsearch integration. Elasticsearch AWS IAM."
msgstr "Integração com Elasticsearch. Elasticsearch AWS IAM."
msgid "Email"
diff --git a/locale/ro_RO/gitlab.po b/locale/ro_RO/gitlab.po
index 49613a3f462..205d65d48ea 100644
--- a/locale/ro_RO/gitlab.po
+++ b/locale/ro_RO/gitlab.po
@@ -2811,7 +2811,7 @@ msgstr ""
msgid "Elasticsearch"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Elasticsearch integration. Elasticsearch AWS IAM."
msgstr ""
msgid "Email"
diff --git a/locale/ru/gitlab.po b/locale/ru/gitlab.po
index b8a4805498b..5075f97bcee 100644
--- a/locale/ru/gitlab.po
+++ b/locale/ru/gitlab.po
@@ -2845,7 +2845,7 @@ msgstr ""
msgid "Elasticsearch"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Elasticsearch integration. Elasticsearch AWS IAM."
msgstr ""
msgid "Email"
diff --git a/locale/sq_AL/gitlab.po b/locale/sq_AL/gitlab.po
index 633b2a6bda7..34281bbc387 100644
--- a/locale/sq_AL/gitlab.po
+++ b/locale/sq_AL/gitlab.po
@@ -2777,7 +2777,7 @@ msgstr ""
msgid "Elasticsearch"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Elasticsearch integration. Elasticsearch AWS IAM."
msgstr ""
msgid "Email"
diff --git a/locale/tr_TR/gitlab.po b/locale/tr_TR/gitlab.po
index 723f4876053..14e2244f06e 100644
--- a/locale/tr_TR/gitlab.po
+++ b/locale/tr_TR/gitlab.po
@@ -2777,7 +2777,7 @@ msgstr ""
msgid "Elasticsearch"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Elasticsearch integration. Elasticsearch AWS IAM."
msgstr ""
msgid "Email"
diff --git a/locale/uk/gitlab.po b/locale/uk/gitlab.po
index 99ba26152ee..992a32dad39 100644
--- a/locale/uk/gitlab.po
+++ b/locale/uk/gitlab.po
@@ -2845,7 +2845,7 @@ msgstr "Редагувати ідентифікацію для %{user_name}"
msgid "Elasticsearch"
msgstr "Elasticsearch"
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Elasticsearch integration. Elasticsearch AWS IAM."
msgstr "Інтеграція з Elasticsearch. Elasticsearch AWS IAM."
msgid "Email"
diff --git a/locale/zh_CN/gitlab.po b/locale/zh_CN/gitlab.po
index 2d3aeca7cc7..808cea98bd1 100644
--- a/locale/zh_CN/gitlab.po
+++ b/locale/zh_CN/gitlab.po
@@ -2743,7 +2743,7 @@ msgstr "编辑 %{user_name} 的身份信息"
msgid "Elasticsearch"
msgstr "Elasticsearch"
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Elasticsearch integration. Elasticsearch AWS IAM."
msgstr "Elasticsearch集成及Elasticsearch AWS IAM。"
msgid "Email"
diff --git a/locale/zh_HK/gitlab.po b/locale/zh_HK/gitlab.po
index d8fe2d5b13e..86647446724 100644
--- a/locale/zh_HK/gitlab.po
+++ b/locale/zh_HK/gitlab.po
@@ -2743,7 +2743,7 @@ msgstr ""
msgid "Elasticsearch"
msgstr ""
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Elasticsearch integration. Elasticsearch AWS IAM."
msgstr ""
msgid "Email"
diff --git a/locale/zh_TW/gitlab.po b/locale/zh_TW/gitlab.po
index 1bccf6de738..5dc9bcb4b2f 100644
--- a/locale/zh_TW/gitlab.po
+++ b/locale/zh_TW/gitlab.po
@@ -2743,7 +2743,7 @@ msgstr "編輯 %{user_name} 的身份"
msgid "Elasticsearch"
msgstr "Elasticsearch"
-msgid "Elasticsearch intergration. Elasticsearch AWS IAM."
+msgid "Elasticsearch integration. Elasticsearch AWS IAM."
msgstr "Elasticsearch 整合。 Elasticsearch AWS IAM。"
msgid "Email"
diff --git a/package.json b/package.json
index d418147b92b..2d6479fea3f 100644
--- a/package.json
+++ b/package.json
@@ -24,8 +24,8 @@
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-syntax-import-meta": "^7.0.0",
"@babel/preset-env": "^7.1.0",
- "@gitlab-org/gitlab-svgs": "^1.33.0",
- "@gitlab-org/gitlab-ui": "^1.9.0",
+ "@gitlab-org/gitlab-ui": "^1.10.0",
+ "@gitlab/svgs": "^1.35.0",
"autosize": "^4.0.0",
"axios": "^0.17.1",
"babel-loader": "^8.0.4",
diff --git a/qa/qa.rb b/qa/qa.rb
index 7be7194f7b9..c0d5244dbfa 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -36,43 +36,40 @@ module QA
##
# GitLab QA fabrication mechanisms
#
- module Factory
- autoload :ApiFabricator, 'qa/factory/api_fabricator'
- autoload :Base, 'qa/factory/base'
- autoload :Product, 'qa/factory/product'
-
- module Resource
- autoload :Sandbox, 'qa/factory/resource/sandbox'
- autoload :Group, 'qa/factory/resource/group'
- autoload :Issue, 'qa/factory/resource/issue'
- autoload :Project, 'qa/factory/resource/project'
- autoload :Label, 'qa/factory/resource/label'
- autoload :MergeRequest, 'qa/factory/resource/merge_request'
- autoload :ProjectImportedFromGithub, 'qa/factory/resource/project_imported_from_github'
- autoload :MergeRequestFromFork, 'qa/factory/resource/merge_request_from_fork'
- autoload :DeployKey, 'qa/factory/resource/deploy_key'
- autoload :DeployToken, 'qa/factory/resource/deploy_token'
- autoload :Branch, 'qa/factory/resource/branch'
- autoload :CiVariable, 'qa/factory/resource/ci_variable'
- autoload :Runner, 'qa/factory/resource/runner'
- autoload :PersonalAccessToken, 'qa/factory/resource/personal_access_token'
- autoload :KubernetesCluster, 'qa/factory/resource/kubernetes_cluster'
- autoload :User, 'qa/factory/resource/user'
- autoload :ProjectMilestone, 'qa/factory/resource/project_milestone'
- autoload :Wiki, 'qa/factory/resource/wiki'
- autoload :File, 'qa/factory/resource/file'
- autoload :Fork, 'qa/factory/resource/fork'
- autoload :SSHKey, 'qa/factory/resource/ssh_key'
- end
+ module Resource
+ autoload :ApiFabricator, 'qa/resource/api_fabricator'
+ autoload :Base, 'qa/resource/base'
+
+ autoload :Sandbox, 'qa/resource/sandbox'
+ autoload :Group, 'qa/resource/group'
+ autoload :Issue, 'qa/resource/issue'
+ autoload :Project, 'qa/resource/project'
+ autoload :Label, 'qa/resource/label'
+ autoload :MergeRequest, 'qa/resource/merge_request'
+ autoload :ProjectImportedFromGithub, 'qa/resource/project_imported_from_github'
+ autoload :MergeRequestFromFork, 'qa/resource/merge_request_from_fork'
+ autoload :DeployKey, 'qa/resource/deploy_key'
+ autoload :DeployToken, 'qa/resource/deploy_token'
+ autoload :Branch, 'qa/resource/branch'
+ autoload :CiVariable, 'qa/resource/ci_variable'
+ autoload :Runner, 'qa/resource/runner'
+ autoload :PersonalAccessToken, 'qa/resource/personal_access_token'
+ autoload :KubernetesCluster, 'qa/resource/kubernetes_cluster'
+ autoload :User, 'qa/resource/user'
+ autoload :ProjectMilestone, 'qa/resource/project_milestone'
+ autoload :Wiki, 'qa/resource/wiki'
+ autoload :File, 'qa/resource/file'
+ autoload :Fork, 'qa/resource/fork'
+ autoload :SSHKey, 'qa/resource/ssh_key'
module Repository
- autoload :Push, 'qa/factory/repository/push'
- autoload :ProjectPush, 'qa/factory/repository/project_push'
- autoload :WikiPush, 'qa/factory/repository/wiki_push'
+ autoload :Push, 'qa/resource/repository/push'
+ autoload :ProjectPush, 'qa/resource/repository/project_push'
+ autoload :WikiPush, 'qa/resource/repository/wiki_push'
end
module Settings
- autoload :HashedStorage, 'qa/factory/settings/hashed_storage'
+ autoload :HashedStorage, 'qa/resource/settings/hashed_storage'
end
end
diff --git a/qa/qa/factory/README.md b/qa/qa/factory/README.md
deleted file mode 100644
index cfce096ab39..00000000000
--- a/qa/qa/factory/README.md
+++ /dev/null
@@ -1,445 +0,0 @@
-# Factory objects in GitLab QA
-
-In GitLab QA we are using factories to create resources.
-
-Factories implementation are primarily done using Browser UI steps, but can also
-be done via the API.
-
-## Why do we need that?
-
-We need factory objects because we need to reduce duplication when creating
-resources for our QA tests.
-
-## How to properly implement a factory object?
-
-All factories should inherit from [`Factory::Base`](./base.rb).
-
-There is only one mandatory method to implement to define a factory. This is the
-`#fabricate!` method, which is used to build a resource via the browser UI.
-Note that you should only use [Page objects](../page/README.md) to interact with
-a Web page in this method.
-
-Here is an imaginary example:
-
-```ruby
-module QA
- module Factory
- module Resource
- class Shirt < Factory::Base
- attr_accessor :name
-
- def fabricate!
- Page::Dashboard::Index.perform do |dashboard_index|
- dashboard_index.go_to_new_shirt
- end
-
- Page::Shirt::New.perform do |shirt_new|
- shirt_new.set_name(name)
- shirt_new.create_shirt!
- end
- end
- end
- end
- end
-end
-```
-
-### Define API implementation
-
-A factory may also implement the three following methods to be able to create a
-resource via the public GitLab API:
-
-- `#api_get_path`: The `GET` path to fetch an existing resource.
-- `#api_post_path`: The `POST` path to create a new resource.
-- `#api_post_body`: The `POST` body (as a Ruby hash) to create a new resource.
-
-Let's take the `Shirt` factory example, and add these three API methods:
-
-```ruby
-module QA
- module Factory
- module Resource
- class Shirt < Factory::Base
- attr_accessor :name
-
- def fabricate!
- # ... same as before
- end
-
- def api_get_path
- "/shirt/#{name}"
- end
-
- def api_post_path
- "/shirts"
- end
-
- def api_post_body
- {
- name: name
- }
- end
- end
- end
- end
-end
-```
-
-The [`Project` factory](./resource/project.rb) is a good real example of Browser
-UI and API implementations.
-
-### Define attributes
-
-After the resource is fabricated, we would like to access the attributes on
-the resource. We define the attributes with `attribute` method. Suppose
-we want to access the name on the resource, we could change `attr_accessor`
-to `attribute`:
-
-```ruby
-module QA
- module Factory
- module Resource
- class Shirt < Factory::Base
- attribute :name
-
- # ... same as before
- end
- end
- end
-end
-```
-
-The difference between `attr_accessor` and `attribute` is that by using
-`attribute` it can also be accessed from the product:
-
-```ruby
-shirt =
- QA::Factory::Resource::Shirt.fabricate! do |resource|
- resource.name = "GitLab QA"
- end
-
-shirt.name # => "GitLab QA"
-```
-
-In the above example, if we use `attr_accessor :name` then `shirt.name` won't
-be available. On the other hand, using `attribute :name` will allow you to use
-`shirt.name`, so most of the time you'll want to use `attribute` instead of
-`attr_accessor` unless we clearly don't need it for the product.
-
-#### Resource attributes
-
-A resource may need another resource to exist first. For instance, a project
-needs a group to be created in.
-
-To define a resource attribute, you can use the `attribute` method with a
-block using the other factory to fabricate the resource.
-
-That will allow access to the other resource from your resource object's
-methods. You would usually use it in `#fabricate!`, `#api_get_path`,
-`#api_post_path`, `#api_post_body`.
-
-Let's take the `Shirt` factory, and add a `project` attribute to it:
-
-```ruby
-module QA
- module Factory
- module Resource
- class Shirt < Factory::Base
- attribute :name
-
- attribute :project do
- Factory::Resource::Project.fabricate! do |resource|
- resource.name = 'project-to-create-a-shirt'
- end
- end
-
- def fabricate!
- project.visit!
-
- Page::Project::Show.perform do |project_show|
- project_show.go_to_new_shirt
- end
-
- Page::Shirt::New.perform do |shirt_new|
- shirt_new.set_name(name)
- shirt_new.create_shirt!
- end
- end
-
- def api_get_path
- "/project/#{project.path}/shirt/#{name}"
- end
-
- def api_post_path
- "/project/#{project.path}/shirts"
- end
-
- def api_post_body
- {
- name: name
- }
- end
- end
- end
- end
-end
-```
-
-**Note that all the attributes are lazily constructed. This means if you want
-a specific attribute to be fabricated first, you'll need to call the
-attribute method first even if you're not using it.**
-
-#### Product data attributes
-
-Once created, you may want to populate a resource with attributes that can be
-found in the Web page, or in the API response.
-For instance, once you create a project, you may want to store its repository
-SSH URL as an attribute.
-
-Again we could use the `attribute` method with a block, using a page object
-to retrieve the data on the page.
-
-Let's take the `Shirt` factory, and define a `:brand` attribute:
-
-```ruby
-module QA
- module Factory
- module Resource
- class Shirt < Factory::Base
- attribute :name
-
- attribute :project do
- Factory::Resource::Project.fabricate! do |resource|
- resource.name = 'project-to-create-a-shirt'
- end
- end
-
- # Attribute populated from the Browser UI (using the block)
- attribute :brand do
- Page::Shirt::Show.perform do |shirt_show|
- shirt_show.fetch_brand_from_page
- end
- end
-
- # ... same as before
- end
- end
- end
-end
-```
-
-**Note again that all the attributes are lazily constructed. This means if
-you call `shirt.brand` after moving to the other page, it'll not properly
-retrieve the data because we're no longer on the expected page.**
-
-Consider this:
-
-```ruby
-shirt =
- QA::Factory::Resource::Shirt.fabricate! do |resource|
- resource.name = "GitLab QA"
- end
-
-shirt.project.visit!
-
-shirt.brand # => FAIL!
-```
-
-The above example will fail because now we're on the project page, trying to
-construct the brand data from the shirt page, however we moved to the project
-page already. There are two ways to solve this, one is that we could try to
-retrieve the brand before visiting the project again:
-
-```ruby
-shirt =
- QA::Factory::Resource::Shirt.fabricate! do |resource|
- resource.name = "GitLab QA"
- end
-
-shirt.brand # => OK!
-
-shirt.project.visit!
-
-shirt.brand # => OK!
-```
-
-The attribute will be stored in the instance therefore all the following calls
-will be fine, using the data previously constructed. If we think that this
-might be too brittle, we could eagerly construct the data right before
-ending fabrication:
-
-```ruby
-module QA
- module Factory
- module Resource
- class Shirt < Factory::Base
- # ... same as before
-
- def fabricate!
- project.visit!
-
- Page::Project::Show.perform do |project_show|
- project_show.go_to_new_shirt
- end
-
- Page::Shirt::New.perform do |shirt_new|
- shirt_new.set_name(name)
- shirt_new.create_shirt!
- end
-
- brand # Eagerly construct the data
- end
- end
- end
- end
-end
-```
-
-This will make sure we construct the data right after we created the shirt.
-The drawback for this will become we're forced to construct the data even
-if we don't really need to use it.
-
-Alternatively, we could just make sure we're on the right page before
-constructing the brand data:
-
-```ruby
-module QA
- module Factory
- module Resource
- class Shirt < Factory::Base
- attribute :name
-
- attribute :project do
- Factory::Resource::Project.fabricate! do |resource|
- resource.name = 'project-to-create-a-shirt'
- end
- end
-
- # Attribute populated from the Browser UI (using the block)
- attribute :brand do
- back_url = current_url
- visit!
-
- Page::Shirt::Show.perform do |shirt_show|
- shirt_show.fetch_brand_from_page
- end
-
- visit(back_url)
- end
-
- # ... same as before
- end
- end
- end
-end
-```
-
-This will make sure it's on the shirt page before constructing brand, and
-move back to the previous page to avoid breaking the state.
-
-#### Define an attribute based on an API response
-
-Sometimes, you want to define a resource attribute based on the API response
-from its `GET` or `POST` request. For instance, if the creation of a shirt via
-the API returns
-
-```ruby
-{
- brand: 'a-brand-new-brand',
- style: 't-shirt',
- materials: [[:cotton, 80], [:polyamide, 20]]
-}
-```
-
-you may want to store `style` as-is in the resource, and fetch the first value
-of the first `materials` item in a `main_fabric` attribute.
-
-Let's take the `Shirt` factory, and define a `:style` and a `:main_fabric`
-attributes:
-
-```ruby
-module QA
- module Factory
- module Resource
- class Shirt < Factory::Base
- # ... same as before
-
- # Attribute from the Shirt factory if present,
- # or fetched from the API response if present,
- # or a QA::Factory::Base::NoValueError is raised otherwise
- attribute :style
-
- # If the attribute from the Shirt factory is not present,
- # and if the API does not contain this field, this block will be
- # used to construct the value based on the API response.
- attribute :main_fabric do
- api_response.&dig(:materials, 0, 0)
- end
-
- # ... same as before
- end
- end
- end
-end
-```
-
-**Notes on attributes precedence:**
-
-- attributes from the factory have the highest precedence
-- attributes from the API response take precedence over attributes from the
- block (usually from Browser UI)
-- attributes without a value will raise a `QA::Factory::Base::NoValueError` error
-
-## Creating resources in your tests
-
-To create a resource in your tests, you can call the `.fabricate!` method on the
-factory class.
-Note that if the factory supports API fabrication, this will use this
-fabrication by default.
-
-Here is an example that will use the API fabrication method under the hood since
-it's supported by the `Shirt` factory:
-
-```ruby
-my_shirt = Factory::Resource::Shirt.fabricate! do |shirt|
- shirt.name = 'my-shirt'
-end
-
-expect(page).to have_text(my_shirt.name) # => "my-shirt" from the factory's attribute
-expect(page).to have_text(my_shirt.brand) # => "a-brand-new-brand" from the API response
-expect(page).to have_text(my_shirt.style) # => "t-shirt" from the API response
-expect(page).to have_text(my_shirt.main_fabric) # => "cotton" from the API response via the block
-```
-
-If you explicitly want to use the Browser UI fabrication method, you can call
-the `.fabricate_via_browser_ui!` method instead:
-
-```ruby
-my_shirt = Factory::Resource::Shirt.fabricate_via_browser_ui! do |shirt|
- shirt.name = 'my-shirt'
-end
-
-expect(page).to have_text(my_shirt.name) # => "my-shirt" from the factory's attribute
-expect(page).to have_text(my_shirt.brand) # => the brand name fetched from the `Page::Shirt::Show` page via the block
-expect(page).to have_text(my_shirt.style) # => QA::Factory::Base::NoValueError will be raised because no API response nor a block is provided
-expect(page).to have_text(my_shirt.main_fabric) # => QA::Factory::Base::NoValueError will be raised because no API response and the block didn't provide a value (because it's also based on the API response)
-```
-
-You can also explicitly use the API fabrication method, by calling the
-`.fabricate_via_api!` method:
-
-```ruby
-my_shirt = Factory::Resource::Shirt.fabricate_via_api! do |shirt|
- shirt.name = 'my-shirt'
-end
-```
-
-In this case, the result will be similar to calling `Factory::Resource::Shirt.fabricate!`.
-
-## Where to ask for help?
-
-If you need more information, ask for help on `#quality` channel on Slack
-(internal, GitLab Team only).
-
-If you are not a Team Member, and you still need help to contribute, please
-open an issue in GitLab CE issue tracker with the `~QA` label.
diff --git a/qa/qa/factory/product.rb b/qa/qa/factory/product.rb
deleted file mode 100644
index 34df0bda8e5..00000000000
--- a/qa/qa/factory/product.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-require 'capybara/dsl'
-
-module QA
- module Factory
- class Product
- include Capybara::DSL
-
- attr_reader :factory
-
- def initialize(factory)
- @factory = factory
-
- define_attributes
- end
-
- def visit!
- visit(web_url)
- end
-
- def populate(*attributes)
- attributes.each(&method(:public_send))
- end
-
- private
-
- def define_attributes
- factory.class.attributes_names.each do |name|
- define_singleton_method(name) do
- factory.public_send(name)
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/branch.rb b/qa/qa/factory/resource/branch.rb
deleted file mode 100644
index b05d1e252ec..00000000000
--- a/qa/qa/factory/resource/branch.rb
+++ /dev/null
@@ -1,77 +0,0 @@
-module QA
- module Factory
- module Resource
- class Branch < Factory::Base
- attr_accessor :project, :branch_name,
- :allow_to_push, :allow_to_merge, :protected
-
- attribute :project do
- Factory::Resource::Project.fabricate! do |resource|
- resource.name = 'protected-branch-project'
- end
- end
-
- def initialize
- @branch_name = 'test/branch'
- @allow_to_push = true
- @allow_to_merge = true
- @protected = false
- end
-
- def fabricate!
- project.visit!
-
- Factory::Repository::ProjectPush.fabricate! do |resource|
- resource.project = project
- resource.file_name = 'kick-off.txt'
- resource.commit_message = 'First commit'
- end
-
- branch = Factory::Repository::ProjectPush.fabricate! do |resource|
- resource.project = project
- resource.file_name = 'README.md'
- resource.commit_message = 'Add readme'
- resource.branch_name = 'master'
- resource.new_branch = false
- resource.remote_branch = @branch_name
- end
-
- Page::Project::Show.perform do |page|
- page.wait { page.has_content?(branch_name) }
- end
-
- # The upcoming process will make it access the Protected Branches page,
- # select the already created branch and protect it according
- # to `allow_to_push` variable.
- return branch unless @protected
-
- Page::Project::Menu.perform(&:click_repository_settings)
-
- Page::Project::Settings::Repository.perform do |setting|
- setting.expand_protected_branches do |page|
- page.select_branch(branch_name)
-
- if allow_to_push
- page.allow_devs_and_maintainers_to_push
- else
- page.allow_no_one_to_push
- end
-
- if allow_to_merge
- page.allow_devs_and_maintainers_to_merge
- else
- page.allow_no_one_to_merge
- end
-
- page.wait(reload: false) do
- !page.first('.btn-success').disabled?
- end
-
- page.protect_branch
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/ci_variable.rb b/qa/qa/factory/resource/ci_variable.rb
deleted file mode 100644
index a0aefc61f9f..00000000000
--- a/qa/qa/factory/resource/ci_variable.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-module QA
- module Factory
- module Resource
- class CiVariable < Factory::Base
- attr_accessor :key, :value
-
- attribute :project do
- Factory::Resource::Project.fabricate! do |resource|
- resource.name = 'project-with-ci-variables'
- resource.description = 'project for adding CI variable test'
- end
- end
-
- def fabricate!
- project.visit!
-
- Page::Project::Menu.perform(&:click_ci_cd_settings)
-
- Page::Project::Settings::CICD.perform do |setting|
- setting.expand_ci_variables do |page|
- page.fill_variable(key, value)
-
- page.save_variables
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/deploy_key.rb b/qa/qa/factory/resource/deploy_key.rb
deleted file mode 100644
index aea99c9f80d..00000000000
--- a/qa/qa/factory/resource/deploy_key.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-module QA
- module Factory
- module Resource
- class DeployKey < Factory::Base
- attr_accessor :title, :key
-
- attribute :fingerprint do
- Page::Project::Settings::Repository.perform do |setting|
- setting.expand_deploy_keys do |key|
- key_offset = key.key_titles.index do |key_title|
- key_title.text == title
- end
-
- key.key_fingerprints[key_offset].text
- end
- end
- end
-
- attribute :project do
- Factory::Resource::Project.fabricate! do |resource|
- resource.name = 'project-to-deploy'
- resource.description = 'project for adding deploy key test'
- end
- end
-
- def fabricate!
- project.visit!
-
- Page::Project::Menu.perform(&:click_repository_settings)
-
- Page::Project::Settings::Repository.perform do |setting|
- setting.expand_deploy_keys do |page|
- page.fill_key_title(title)
- page.fill_key_value(key)
-
- page.add_key
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/deploy_token.rb b/qa/qa/factory/resource/deploy_token.rb
deleted file mode 100644
index 68e98f0aa01..00000000000
--- a/qa/qa/factory/resource/deploy_token.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-module QA
- module Factory
- module Resource
- class DeployToken < Factory::Base
- attr_accessor :name, :expires_at
-
- attribute :username do
- Page::Project::Settings::Repository.perform do |page|
- page.expand_deploy_tokens do |token|
- token.token_username
- end
- end
- end
-
- attribute :password do
- Page::Project::Settings::Repository.perform do |page|
- page.expand_deploy_tokens do |token|
- token.token_password
- end
- end
- end
-
- attribute :project do
- Factory::Resource::Project.fabricate! do |resource|
- resource.name = 'project-to-deploy'
- resource.description = 'project for adding deploy token test'
- end
- end
-
- def fabricate!
- project.visit!
-
- Page::Project::Menu.act do
- click_repository_settings
- end
-
- Page::Project::Settings::Repository.perform do |setting|
- setting.expand_deploy_tokens do |page|
- page.fill_token_name(name)
- page.fill_token_expires_at(expires_at)
- page.fill_scopes(read_repository: true, read_registry: false)
-
- page.add_token
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/file.rb b/qa/qa/factory/resource/file.rb
deleted file mode 100644
index 1148876c2d3..00000000000
--- a/qa/qa/factory/resource/file.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-module QA
- module Factory
- module Resource
- class File < Factory::Base
- attr_accessor :name,
- :content,
- :commit_message
-
- attribute :project do
- Factory::Resource::Project.fabricate! do |resource|
- resource.name = 'project-with-new-file'
- end
- end
-
- def initialize
- @name = 'QA Test - File name'
- @content = 'QA Test - File content'
- @commit_message = 'QA Test - Commit message'
- end
-
- def fabricate!
- project.visit!
-
- Page::Project::Show.perform(&:create_new_file!)
-
- Page::File::Form.perform do |page|
- page.add_name(@name)
- page.add_content(@content)
- page.add_commit_message(@commit_message)
- page.commit_changes
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/fork.rb b/qa/qa/factory/resource/fork.rb
deleted file mode 100644
index 0fac4377040..00000000000
--- a/qa/qa/factory/resource/fork.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-module QA
- module Factory
- module Resource
- class Fork < Factory::Base
- attribute :push do
- Factory::Repository::ProjectPush.fabricate!
- end
-
- attribute :user do
- Factory::Resource::User.fabricate! do |resource|
- if Runtime::Env.forker?
- resource.username = Runtime::Env.forker_username
- resource.password = Runtime::Env.forker_password
- end
- end
- end
-
- def visit_project_with_retry
- # The user intermittently fails to stay signed in after visiting the
- # project page. The new user is registered and then signs in and a
- # screenshot shows that signing in was successful. Then the project
- # page is visited but a screenshot shows the user is no longer signed
- # in. It's difficult to reproduce locally but GDK logs don't seem to
- # show anything unexpected. This method attempts to work around the
- # problem and capture data to help troubleshoot.
-
- Capybara::Screenshot.screenshot_and_save_page
-
- start = Time.now
-
- while Time.now - start < 20
- push.project.visit!
-
- puts "Visited project page"
- Capybara::Screenshot.screenshot_and_save_page
-
- return if Page::Main::Menu.act { has_personal_area?(wait: 0) }
-
- puts "Not signed in. Attempting to sign in again."
- Capybara::Screenshot.screenshot_and_save_page
-
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
-
- Page::Main::Login.perform do |login|
- login.sign_in_using_credentials(user)
- end
- end
-
- raise "Failed to load project page and stay logged in"
- end
-
- def fabricate!
- push
- user
-
- visit_project_with_retry
-
- Page::Project::Show.perform(&:fork_project)
-
- Page::Project::Fork::New.perform do |fork_new|
- fork_new.choose_namespace(user.name)
- end
-
- Page::Layout::Banner.perform do |page|
- page.has_notice?('The project was successfully forked.')
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/group.rb b/qa/qa/factory/resource/group.rb
deleted file mode 100644
index 45e49da86f9..00000000000
--- a/qa/qa/factory/resource/group.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-module QA
- module Factory
- module Resource
- class Group < Factory::Base
- attr_accessor :path, :description
-
- attribute :sandbox do
- Factory::Resource::Sandbox.fabricate!
- end
-
- attribute :id
-
- def initialize
- @path = Runtime::Namespace.name
- @description = "QA test run at #{Runtime::Namespace.time}"
- end
-
- def fabricate!
- sandbox.visit!
-
- Page::Group::Show.perform do |group_show|
- if group_show.has_subgroup?(path)
- group_show.go_to_subgroup(path)
- else
- group_show.go_to_new_subgroup
-
- Page::Group::New.perform do |group_new|
- group_new.set_path(path)
- group_new.set_description(description)
- group_new.set_visibility('Public')
- group_new.create
- end
-
- # Ensure that the group was actually created
- group_show.wait(time: 1) do
- group_show.has_text?(path) &&
- group_show.has_new_project_or_subgroup_dropdown?
- end
- end
- end
- end
-
- def fabricate_via_api!
- resource_web_url(api_get)
- rescue ResourceNotFoundError
- super
- end
-
- def api_get_path
- "/groups/#{CGI.escape("#{sandbox.path}/#{path}")}"
- end
-
- def api_post_path
- '/groups'
- end
-
- def api_post_body
- {
- parent_id: sandbox.id,
- path: path,
- name: path,
- visibility: 'public'
- }
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/issue.rb b/qa/qa/factory/resource/issue.rb
deleted file mode 100644
index 3a28e0d5aa6..00000000000
--- a/qa/qa/factory/resource/issue.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-module QA
- module Factory
- module Resource
- class Issue < Factory::Base
- attr_writer :description
-
- attribute :project do
- Factory::Resource::Project.fabricate! do |resource|
- resource.name = 'project-for-issues'
- resource.description = 'project for adding issues'
- end
- end
-
- attribute :title
-
- def fabricate!
- project.visit!
-
- Page::Project::Show.perform(&:go_to_new_issue)
-
- Page::Project::Issue::New.perform do |page|
- page.add_title(@title)
- page.add_description(@description)
- page.create_new_issue
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/kubernetes_cluster.rb b/qa/qa/factory/resource/kubernetes_cluster.rb
deleted file mode 100644
index aac6864f42f..00000000000
--- a/qa/qa/factory/resource/kubernetes_cluster.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-require 'securerandom'
-
-module QA
- module Factory
- module Resource
- class KubernetesCluster < Factory::Base
- attr_writer :project, :cluster,
- :install_helm_tiller, :install_ingress, :install_prometheus, :install_runner
-
- attribute :ingress_ip do
- Page::Project::Operations::Kubernetes::Show.perform(&:ingress_ip)
- end
-
- def fabricate!
- @project.visit!
-
- Page::Project::Menu.perform(
- &:click_operations_kubernetes)
-
- Page::Project::Operations::Kubernetes::Index.perform(
- &:add_kubernetes_cluster)
-
- Page::Project::Operations::Kubernetes::Add.perform(
- &:add_existing_cluster)
-
- Page::Project::Operations::Kubernetes::AddExisting.perform do |page|
- page.set_cluster_name(@cluster.cluster_name)
- page.set_api_url(@cluster.api_url)
- page.set_ca_certificate(@cluster.ca_certificate)
- page.set_token(@cluster.token)
- page.check_rbac! if @cluster.rbac
- page.add_cluster!
- end
-
- if @install_helm_tiller
- Page::Project::Operations::Kubernetes::Show.perform do |page|
- # We must wait a few seconds for permissions to be set up correctly for new cluster
- sleep 10
-
- # Helm must be installed before everything else
- page.install!(:helm)
- page.await_installed(:helm)
-
- page.install!(:ingress) if @install_ingress
- page.install!(:prometheus) if @install_prometheus
- page.install!(:runner) if @install_runner
-
- page.await_installed(:ingress) if @install_ingress
- page.await_installed(:prometheus) if @install_prometheus
- page.await_installed(:runner) if @install_runner
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/label.rb b/qa/qa/factory/resource/label.rb
deleted file mode 100644
index 32bc519b48c..00000000000
--- a/qa/qa/factory/resource/label.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-require 'securerandom'
-
-module QA
- module Factory
- module Resource
- class Label < Factory::Base
- attr_accessor :description, :color
-
- attribute :title
-
- attribute :project do
- Factory::Resource::Project.fabricate! do |resource|
- resource.name = 'project-with-label'
- end
- end
-
- def initialize
- @title = "qa-test-#{SecureRandom.hex(8)}"
- @description = 'This is a test label'
- @color = '#0033CC'
- end
-
- def fabricate!
- project.visit!
-
- Page::Project::Menu.perform(&:go_to_labels)
- Page::Label::Index.perform(&:go_to_new_label)
-
- Page::Label::New.perform do |page|
- page.fill_title(@title)
- page.fill_description(@description)
- page.fill_color(@color)
- page.create_label
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/merge_request.rb b/qa/qa/factory/resource/merge_request.rb
deleted file mode 100644
index 92b8bdf4a21..00000000000
--- a/qa/qa/factory/resource/merge_request.rb
+++ /dev/null
@@ -1,73 +0,0 @@
-require 'securerandom'
-
-module QA
- module Factory
- module Resource
- class MergeRequest < Factory::Base
- attr_accessor :title,
- :description,
- :source_branch,
- :target_branch,
- :assignee,
- :milestone,
- :labels
-
- attribute :source_branch
-
- attribute :project do
- Factory::Resource::Project.fabricate! do |resource|
- resource.name = 'project-with-merge-request'
- end
- end
-
- attribute :target do
- project.visit!
-
- Factory::Repository::ProjectPush.fabricate! do |resource|
- resource.project = project
- resource.branch_name = 'master'
- resource.remote_branch = target_branch
- end
- end
-
- attribute :source do
- Factory::Repository::ProjectPush.fabricate! do |resource|
- resource.project = project
- resource.branch_name = target_branch
- resource.remote_branch = source_branch
- resource.new_branch = false
- resource.file_name = "added_file.txt"
- resource.file_content = "File Added"
- end
- end
-
- def initialize
- @title = 'QA test - merge request'
- @description = 'This is a test merge request'
- @source_branch = "qa-test-feature-#{SecureRandom.hex(8)}"
- @target_branch = "master"
- @assignee = nil
- @milestone = nil
- @labels = []
- end
-
- def fabricate!
- target
- source
- project.visit!
- Page::Project::Show.perform(&:new_merge_request)
- Page::MergeRequest::New.perform do |page|
- page.fill_title(@title)
- page.fill_description(@description)
- page.choose_milestone(@milestone) if @milestone
- labels.each do |label|
- page.select_label(label)
- end
-
- page.create_merge_request
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/merge_request_from_fork.rb b/qa/qa/factory/resource/merge_request_from_fork.rb
deleted file mode 100644
index fbe062539b9..00000000000
--- a/qa/qa/factory/resource/merge_request_from_fork.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-module QA
- module Factory
- module Resource
- class MergeRequestFromFork < MergeRequest
- attr_accessor :fork_branch
-
- attribute :fork do
- Factory::Resource::Fork.fabricate!
- end
-
- attribute :push do
- Factory::Repository::ProjectPush.fabricate! do |resource|
- resource.project = fork
- resource.branch_name = fork_branch
- resource.file_name = 'file2.txt'
- resource.user = fork.user
- end
- end
-
- def fabricate!
- push
- fork.visit!
- Page::Project::Show.perform(&:new_merge_request)
- Page::MergeRequest::New.perform(&:create_merge_request)
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/personal_access_token.rb b/qa/qa/factory/resource/personal_access_token.rb
deleted file mode 100644
index ceb0f1c3d75..00000000000
--- a/qa/qa/factory/resource/personal_access_token.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-module QA
- module Factory
- module Resource
- ##
- # Create a personal access token that can be used by the api
- #
- class PersonalAccessToken < Factory::Base
- attr_accessor :name
-
- attribute :access_token do
- Page::Profile::PersonalAccessTokens.perform(&:created_access_token)
- end
-
- def fabricate!
- Page::Main::Menu.perform(&:go_to_profile_settings)
- Page::Profile::Menu.perform(&:click_access_tokens)
-
- Page::Profile::PersonalAccessTokens.perform do |page|
- page.fill_token_name(name || 'api-test-token')
- page.check_api
- page.create_token
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/project.rb b/qa/qa/factory/resource/project.rb
deleted file mode 100644
index f691ae5a342..00000000000
--- a/qa/qa/factory/resource/project.rb
+++ /dev/null
@@ -1,78 +0,0 @@
-require 'securerandom'
-
-module QA
- module Factory
- module Resource
- class Project < Factory::Base
- attribute :name
- attribute :description
-
- attribute :group do
- Factory::Resource::Group.fabricate!
- end
-
- attribute :repository_ssh_location do
- Page::Project::Show.perform do |page|
- page.choose_repository_clone_ssh
- page.repository_location
- end
- end
-
- attribute :repository_http_location do
- Page::Project::Show.perform do |page|
- page.choose_repository_clone_http
- page.repository_location
- end
- end
-
- def initialize
- @description = 'My awesome project'
- end
-
- def name=(raw_name)
- @name = "#{raw_name}-#{SecureRandom.hex(8)}"
- end
-
- def fabricate!
- group.visit!
-
- Page::Group::Show.perform(&:go_to_new_project)
-
- Page::Project::New.perform do |page|
- page.choose_test_namespace
- page.choose_name(@name)
- page.add_description(@description)
- page.set_visibility('Public')
- page.create_new_project
- end
- end
-
- def api_get_path
- "/projects/#{name}"
- end
-
- def api_post_path
- '/projects'
- end
-
- def api_post_body
- {
- namespace_id: group.id,
- path: name,
- name: name,
- description: description,
- visibility: 'public'
- }
- end
-
- private
-
- def transform_api_resource(resource)
- resource[:repository_ssh_location] = Git::Location.new(resource[:ssh_url_to_repo])
- resource[:repository_http_location] = Git::Location.new(resource[:http_url_to_repo])
- resource
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/project_imported_from_github.rb b/qa/qa/factory/resource/project_imported_from_github.rb
deleted file mode 100644
index f62092ae122..00000000000
--- a/qa/qa/factory/resource/project_imported_from_github.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-require 'securerandom'
-
-module QA
- module Factory
- module Resource
- class ProjectImportedFromGithub < Resource::Project
- attr_writer :personal_access_token, :github_repository_path
-
- attribute :group do
- Factory::Resource::Group.fabricate!
- end
-
- attribute :name
-
- def fabricate!
- group.visit!
-
- Page::Group::Show.perform(&:go_to_new_project)
-
- Page::Project::New.perform do |page|
- page.go_to_import_project
- end
-
- Page::Project::New.perform do |page|
- page.go_to_github_import
- end
-
- Page::Project::Import::Github.perform do |page|
- page.add_personal_access_token(@personal_access_token)
- page.list_repos
- page.import!(@github_repository_path, @name)
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/project_milestone.rb b/qa/qa/factory/resource/project_milestone.rb
deleted file mode 100644
index cfda58dc103..00000000000
--- a/qa/qa/factory/resource/project_milestone.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-module QA
- module Factory
- module Resource
- class ProjectMilestone < Factory::Base
- attr_accessor :description
-
- attribute :project do
- Factory::Resource::Project.fabricate!
- end
-
- attribute :title
-
- def title=(title)
- @title = "#{title}-#{SecureRandom.hex(4)}"
- @description = 'A milestone'
- end
-
- def fabricate!
- project.visit!
-
- Page::Project::Menu.perform do |page|
- page.click_issues
- page.click_milestones
- end
-
- Page::Project::Milestone::Index.perform(&:click_new_milestone)
-
- Page::Project::Milestone::New.perform do |milestone_new|
- milestone_new.set_title(@title)
- milestone_new.set_description(@description)
- milestone_new.create_new_milestone
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/runner.rb b/qa/qa/factory/resource/runner.rb
deleted file mode 100644
index 7108db1e55a..00000000000
--- a/qa/qa/factory/resource/runner.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-require 'securerandom'
-
-module QA
- module Factory
- module Resource
- class Runner < Factory::Base
- attr_writer :name, :tags, :image
-
- attribute :project do
- Factory::Resource::Project.fabricate! do |resource|
- resource.name = 'project-with-ci-cd'
- resource.description = 'Project with CI/CD Pipelines'
- end
- end
-
- def name
- @name || "qa-runner-#{SecureRandom.hex(4)}"
- end
-
- def tags
- @tags || %w[qa e2e]
- end
-
- def image
- @image || 'gitlab/gitlab-runner:alpine'
- end
-
- def fabricate!
- project.visit!
-
- Page::Project::Menu.perform(&:click_ci_cd_settings)
-
- Service::Runner.new(name).tap do |runner|
- Page::Project::Settings::CICD.perform do |settings|
- settings.expand_runners_settings do |runners|
- runner.pull
- runner.token = runners.registration_token
- runner.address = runners.coordinator_address
- runner.tags = tags
- runner.image = image
- runner.register!
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/sandbox.rb b/qa/qa/factory/resource/sandbox.rb
deleted file mode 100644
index 56bcda9e2f3..00000000000
--- a/qa/qa/factory/resource/sandbox.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-module QA
- module Factory
- module Resource
- ##
- # Ensure we're in our sandbox namespace, either by navigating to it or by
- # creating it if it doesn't yet exist.
- #
- class Sandbox < Factory::Base
- attr_reader :path
-
- attribute :id
- attribute :path
-
- def initialize
- @path = Runtime::Namespace.sandbox_name
- end
-
- def fabricate!
- Page::Main::Menu.perform(&:go_to_groups)
-
- Page::Dashboard::Groups.perform do |page|
- if page.has_group?(path)
- page.go_to_group(path)
- else
- page.go_to_new_group
-
- Page::Group::New.perform do |group|
- group.set_path(path)
- group.set_description('GitLab QA Sandbox Group')
- group.set_visibility('Public')
- group.create
- end
- end
- end
- end
-
- def fabricate_via_api!
- resource_web_url(api_get)
- rescue ResourceNotFoundError
- super
- end
-
- def api_get_path
- "/groups/#{path}"
- end
-
- def api_post_path
- '/groups'
- end
-
- def api_post_body
- {
- path: path,
- name: path,
- visibility: 'public'
- }
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/ssh_key.rb b/qa/qa/factory/resource/ssh_key.rb
deleted file mode 100644
index a48a93fbe65..00000000000
--- a/qa/qa/factory/resource/ssh_key.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-module QA
- module Factory
- module Resource
- class SSHKey < Factory::Base
- extend Forwardable
-
- def_delegators :key, :private_key, :public_key, :fingerprint
-
- attribute :private_key
- attribute :title
- attribute :fingerprint
-
- def key
- @key ||= Runtime::Key::RSA.new
- end
-
- def fabricate!
- Page::Main::Menu.perform(&:go_to_profile_settings)
- Page::Profile::Menu.perform(&:click_ssh_keys)
-
- Page::Profile::SSHKeys.perform do |page|
- page.add_key(public_key, title)
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/user.rb b/qa/qa/factory/resource/user.rb
deleted file mode 100644
index 6e6f46f7a95..00000000000
--- a/qa/qa/factory/resource/user.rb
+++ /dev/null
@@ -1,60 +0,0 @@
-require 'securerandom'
-
-module QA
- module Factory
- module Resource
- class User < Factory::Base
- attr_reader :unique_id
-
- def initialize
- @unique_id = SecureRandom.hex(8)
- end
-
- def username
- @username ||= "qa-user-#{unique_id}"
- end
-
- def password
- @password ||= 'password'
- end
-
- def name
- @name ||= username
- end
-
- def email
- @email ||= "#{username}@example.com"
- end
-
- def credentials_given?
- defined?(@username) && defined?(@password)
- end
-
- attribute :name
- attribute :username
- attribute :email
- attribute :password
-
- def fabricate!
- # Don't try to log-out if we're not logged-in
- if Page::Main::Menu.perform { |p| p.has_personal_area?(wait: 0) }
- Page::Main::Menu.perform { |main| main.sign_out }
- end
-
- if credentials_given?
- Page::Main::Login.perform do |login|
- login.sign_in_using_credentials(self)
- end
- else
- Page::Main::Login.perform do |login|
- login.switch_to_register_tab
- end
- Page::Main::SignUp.perform do |signup|
- signup.sign_up!(self)
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/wiki.rb b/qa/qa/factory/resource/wiki.rb
deleted file mode 100644
index 769f394e85c..00000000000
--- a/qa/qa/factory/resource/wiki.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-module QA
- module Factory
- module Resource
- class Wiki < Factory::Base
- attr_accessor :title, :content, :message
-
- attribute :project do
- Factory::Resource::Project.fabricate! do |resource|
- resource.name = 'project-for-wikis'
- resource.description = 'project for adding wikis'
- end
- end
-
- def fabricate!
- project.visit!
-
- Page::Project::Menu.perform { |menu_side| menu_side.click_wiki }
-
- Page::Project::Wiki::New.perform do |wiki_new|
- wiki_new.go_to_create_first_page
- wiki_new.set_title(@title)
- wiki_new.set_content(@content)
- wiki_new.set_message(@message)
- wiki_new.create_new_page
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb
index 27a88534258..7f959441dac 100644
--- a/qa/qa/git/repository.rb
+++ b/qa/qa/git/repository.rb
@@ -109,6 +109,28 @@ module QA
known_hosts_file.close(true)
end
+ def push_with_git_protocol(version, file_name, file_content, commit_message = 'Initial commit')
+ self.git_protocol = version
+ add_file(file_name, file_content)
+ commit(commit_message)
+ push_changes
+
+ fetch_supported_git_protocol
+ end
+
+ def git_protocol=(value)
+ raise ArgumentError, "Please specify the protocol you would like to use: 0, 1, or 2" unless %w[0 1 2].include?(value.to_s)
+
+ run("git config protocol.version #{value}")
+ end
+
+ def fetch_supported_git_protocol
+ # ls-remote is one command known to respond to Git protocol v2 so we use
+ # it to get output including the version reported via Git tracing
+ output = run("git ls-remote #{uri}", "GIT_TRACE_PACKET=1")
+ output[/git< version (\d+)/, 1] || 'unknown'
+ end
+
private
attr_reader :uri, :username, :password, :known_hosts_file, :private_key_file
@@ -117,8 +139,8 @@ module QA
!private_key_file.nil?
end
- def run(command_str)
- command = [env_vars, command_str, '2>&1'].compact.join(' ')
+ def run(command_str, *extra_env)
+ command = [env_vars, *extra_env, command_str, '2>&1'].compact.join(' ')
Runtime::Logger.debug "Git: command=[#{command}]"
output, _ = Open3.capture2(command)
diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb
index 94b9486b0d5..97ffe0e5716 100644
--- a/qa/qa/page/main/login.rb
+++ b/qa/qa/page/main/login.rb
@@ -65,7 +65,7 @@ module QA
end
def sign_in_using_admin_credentials
- admin = QA::Factory::Resource::User.new.tap do |user|
+ admin = QA::Resource::User.new.tap do |user|
user.username = QA::Runtime::User.admin_username
user.password = QA::Runtime::User.admin_password
end
diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb
index 376606afb5d..2fd30e15ffb 100644
--- a/qa/qa/page/merge_request/show.rb
+++ b/qa/qa/page/merge_request/show.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module QA
module Page
module MergeRequest
@@ -23,10 +25,40 @@ module QA
element :squash_checkbox
end
+ view 'app/views/projects/merge_requests/show.html.haml' do
+ element :notes_tab
+ element :diffs_tab
+ end
+
+ view 'app/assets/javascripts/diffs/components/diff_line_gutter_content.vue' do
+ element :diff_comment
+ end
+
+ view 'app/assets/javascripts/notes/components/comment_form.vue' do
+ element :note_dropdown
+ element :discussion_option
+ end
+
+ view 'app/assets/javascripts/notes/components/note_form.vue' do
+ element :reply_input
+ end
+
+ view 'app/assets/javascripts/notes/components/noteable_discussion.vue' do
+ element :discussion_reply
+ end
+
+ view 'app/assets/javascripts/diffs/components/inline_diff_table_row.vue' do
+ element :new_diff_line
+ end
+
view 'app/views/shared/issuable/_sidebar.html.haml' do
element :labels_block
end
+ view 'app/views/projects/merge_requests/_mr_title.html.haml' do
+ element :edit_button
+ end
+
def fast_forward_possible?
!has_text?('Fast-forward merge is not possible')
end
@@ -106,6 +138,39 @@ module QA
click_element :squash_checkbox
end
+
+ def go_to_discussions_tab
+ click_element :notes_tab
+ end
+
+ def go_to_diffs_tab
+ click_element :diffs_tab
+ end
+
+ def add_comment_to_diff(text)
+ wait(time: 5) do
+ page.has_text?("No newline at end of file")
+ end
+ all_elements(:new_diff_line).first.hover
+ click_element :diff_comment
+ fill_element :reply_input, text
+ end
+
+ def start_discussion(text)
+ fill_element :comment_input, text
+ click_element :note_dropdown
+ click_element :discussion_option
+ click_element :comment_button
+ end
+
+ def reply_to_discussion(reply_text)
+ all_elements(:discussion_reply).last.click
+ fill_element :reply_input, reply_text
+ end
+
+ def edit!
+ click_element :edit_button
+ end
end
end
end
diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb
index de18b9cefa6..23def93c7dd 100644
--- a/qa/qa/page/project/issue/show.rb
+++ b/qa/qa/page/project/issue/show.rb
@@ -37,6 +37,11 @@ module QA
def select_comments_only_filter
click_element :discussion_filter
+ all_elements(:filter_options)[1].click
+ end
+
+ def select_history_only_filter
+ click_element :discussion_filter
all_elements(:filter_options).last.click
end
diff --git a/qa/qa/page/project/operations/kubernetes/add.rb b/qa/qa/page/project/operations/kubernetes/add.rb
index 18c16ca6db7..939f912ea85 100644
--- a/qa/qa/page/project/operations/kubernetes/add.rb
+++ b/qa/qa/page/project/operations/kubernetes/add.rb
@@ -4,7 +4,7 @@ module QA
module Operations
module Kubernetes
class Add < Page::Base
- view 'app/views/projects/clusters/new.html.haml' do
+ view 'app/views/clusters/clusters/new.html.haml' do
element :add_existing_cluster_button, "Add existing cluster" # rubocop:disable QA/ElementWithPattern
end
diff --git a/qa/qa/page/project/operations/kubernetes/add_existing.rb b/qa/qa/page/project/operations/kubernetes/add_existing.rb
index f8e026b4405..f3ab636ecc1 100644
--- a/qa/qa/page/project/operations/kubernetes/add_existing.rb
+++ b/qa/qa/page/project/operations/kubernetes/add_existing.rb
@@ -4,7 +4,7 @@ module QA
module Operations
module Kubernetes
class AddExisting < Page::Base
- view 'app/views/projects/clusters/user/_form.html.haml' do
+ view 'app/views/clusters/clusters/user/_form.html.haml' do
element :cluster_name, 'text_field :name' # rubocop:disable QA/ElementWithPattern
element :api_url, 'text_field :api_url' # rubocop:disable QA/ElementWithPattern
element :ca_certificate, 'text_area :ca_cert' # rubocop:disable QA/ElementWithPattern
diff --git a/qa/qa/page/project/operations/kubernetes/index.rb b/qa/qa/page/project/operations/kubernetes/index.rb
index 312b459ac89..67a74af1cd2 100644
--- a/qa/qa/page/project/operations/kubernetes/index.rb
+++ b/qa/qa/page/project/operations/kubernetes/index.rb
@@ -4,7 +4,7 @@ module QA
module Operations
module Kubernetes
class Index < Page::Base
- view 'app/views/projects/clusters/_empty_state.html.haml' do
+ view 'app/views/clusters/clusters/_empty_state.html.haml' do
element :add_kubernetes_cluster_button, "link_to s_('ClusterIntegration|Add Kubernetes cluster')" # rubocop:disable QA/ElementWithPattern
end
diff --git a/qa/qa/resource/README.md b/qa/qa/resource/README.md
new file mode 100644
index 00000000000..4cdeb3f42a2
--- /dev/null
+++ b/qa/qa/resource/README.md
@@ -0,0 +1,392 @@
+# Resource class in GitLab QA
+
+Resources are primarily created using Browser UI steps, but can also
+be created via the API.
+
+## How to properly implement a resource class?
+
+All resource classes should inherit from [`Resource::Base`](./base.rb).
+
+There is only one mandatory method to implement to define a resource class.
+This is the `#fabricate!` method, which is used to build the resource via the
+browser UI. Note that you should only use [Page objects](../page/README.md) to
+interact with a Web page in this method.
+
+Here is an imaginary example:
+
+```ruby
+module QA
+ module Resource
+ class Shirt < Base
+ attr_accessor :name
+
+ def fabricate!
+ Page::Dashboard::Index.perform do |dashboard_index|
+ dashboard_index.go_to_new_shirt
+ end
+
+ Page::Shirt::New.perform do |shirt_new|
+ shirt_new.set_name(name)
+ shirt_new.create_shirt!
+ end
+ end
+ end
+ end
+end
+```
+
+### Define API implementation
+
+A resource class may also implement the three following methods to be able to
+create the resource via the public GitLab API:
+
+- `#api_get_path`: The `GET` path to fetch an existing resource.
+- `#api_post_path`: The `POST` path to create a new resource.
+- `#api_post_body`: The `POST` body (as a Ruby hash) to create a new resource.
+
+Let's take the `Shirt` resource class, and add these three API methods:
+
+```ruby
+module QA
+ module Resource
+ class Shirt < Base
+ attr_accessor :name
+
+ def fabricate!
+ # ... same as before
+ end
+
+ def api_get_path
+ "/shirt/#{name}"
+ end
+
+ def api_post_path
+ "/shirts"
+ end
+
+ def api_post_body
+ {
+ name: name
+ }
+ end
+ end
+ end
+end
+```
+
+The [`Project` resource](./project.rb) is a good real example of Browser
+UI and API implementations.
+
+#### Resource attributes
+
+A resource may need another resource to exist first. For instance, a project
+needs a group to be created in.
+
+To define a resource attribute, you can use the `attribute` method with a
+block using the other resource class to fabricate the resource.
+
+That will allow access to the other resource from your resource object's
+methods. You would usually use it in `#fabricate!`, `#api_get_path`,
+`#api_post_path`, `#api_post_body`.
+
+Let's take the `Shirt` resource class, and add a `project` attribute to it:
+
+```ruby
+module QA
+ module Resource
+ class Shirt < Base
+ attr_accessor :name
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-to-create-a-shirt'
+ end
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Show.perform do |project_show|
+ project_show.go_to_new_shirt
+ end
+
+ Page::Shirt::New.perform do |shirt_new|
+ shirt_new.set_name(name)
+ shirt_new.create_shirt!
+ end
+ end
+
+ def api_get_path
+ "/project/#{project.path}/shirt/#{name}"
+ end
+
+ def api_post_path
+ "/project/#{project.path}/shirts"
+ end
+
+ def api_post_body
+ {
+ name: name
+ }
+ end
+ end
+ end
+end
+```
+
+**Note that all the attributes are lazily constructed. This means if you want
+a specific attribute to be fabricated first, you'll need to call the
+attribute method first even if you're not using it.**
+
+#### Product data attributes
+
+Once created, you may want to populate a resource with attributes that can be
+found in the Web page, or in the API response.
+For instance, once you create a project, you may want to store its repository
+SSH URL as an attribute.
+
+Again we could use the `attribute` method with a block, using a page object
+to retrieve the data on the page.
+
+Let's take the `Shirt` resource class, and define a `:brand` attribute:
+
+```ruby
+module QA
+ module Resource
+ class Shirt < Base
+ attr_accessor :name
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-to-create-a-shirt'
+ end
+ end
+
+ # Attribute populated from the Browser UI (using the block)
+ attribute :brand do
+ Page::Shirt::Show.perform do |shirt_show|
+ shirt_show.fetch_brand_from_page
+ end
+ end
+
+ # ... same as before
+ end
+ end
+end
+```
+
+**Note again that all the attributes are lazily constructed. This means if
+you call `shirt.brand` after moving to the other page, it'll not properly
+retrieve the data because we're no longer on the expected page.**
+
+Consider this:
+
+```ruby
+shirt =
+ QA::Resource::Shirt.fabricate! do |resource|
+ resource.name = "GitLab QA"
+ end
+
+shirt.project.visit!
+
+shirt.brand # => FAIL!
+```
+
+The above example will fail because now we're on the project page, trying to
+construct the brand data from the shirt page, however we moved to the project
+page already. There are two ways to solve this, one is that we could try to
+retrieve the brand before visiting the project again:
+
+```ruby
+shirt =
+ QA::Resource::Shirt.fabricate! do |resource|
+ resource.name = "GitLab QA"
+ end
+
+shirt.brand # => OK!
+
+shirt.project.visit!
+
+shirt.brand # => OK!
+```
+
+The attribute will be stored in the instance therefore all the following calls
+will be fine, using the data previously constructed. If we think that this
+might be too brittle, we could eagerly construct the data right before
+ending fabrication:
+
+```ruby
+module QA
+ module Resource
+ class Shirt < Base
+ # ... same as before
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Show.perform do |project_show|
+ project_show.go_to_new_shirt
+ end
+
+ Page::Shirt::New.perform do |shirt_new|
+ shirt_new.set_name(name)
+ shirt_new.create_shirt!
+ end
+
+ populate(:brand) # Eagerly construct the data
+ end
+ end
+ end
+end
+```
+
+The `populate` method will iterate through its arguments and call each
+attribute respectively. Here `populate(:brand)` has the same effect as
+just `brand`. Using the populate method makes the intention clearer.
+
+With this, it will make sure we construct the data right after we create the
+shirt. The drawback is that this will always construct the data when the
+resource is fabricated even if we don't need to use the data.
+
+Alternatively, we could just make sure we're on the right page before
+constructing the brand data:
+
+```ruby
+module QA
+ module Resource
+ class Shirt < Base
+ attr_accessor :name
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-to-create-a-shirt'
+ end
+ end
+
+ # Attribute populated from the Browser UI (using the block)
+ attribute :brand do
+ back_url = current_url
+ visit!
+
+ Page::Shirt::Show.perform do |shirt_show|
+ shirt_show.fetch_brand_from_page
+ end
+
+ visit(back_url)
+ end
+
+ # ... same as before
+ end
+ end
+end
+```
+
+This will make sure it's on the shirt page before constructing brand, and
+move back to the previous page to avoid breaking the state.
+
+#### Define an attribute based on an API response
+
+Sometimes, you want to define a resource attribute based on the API response
+from its `GET` or `POST` request. For instance, if the creation of a shirt via
+the API returns
+
+```ruby
+{
+ brand: 'a-brand-new-brand',
+ style: 't-shirt',
+ materials: [[:cotton, 80], [:polyamide, 20]]
+}
+```
+
+you may want to store `style` as-is in the resource, and fetch the first value
+of the first `materials` item in a `main_fabric` attribute.
+
+Let's take the `Shirt` resource class, and define a `:style` and a
+`:main_fabric` attributes:
+
+```ruby
+module QA
+ module Resource
+ class Shirt < Base
+ # ... same as before
+
+ # @style from the instance if present,
+ # or fetched from the API response if present,
+ # or a QA::Resource::Base::NoValueError is raised otherwise
+ attribute :style
+
+ # If @main_fabric is not present,
+ # and if the API does not contain this field, this block will be
+ # used to construct the value based on the API response, and
+ # store the result in @main_fabric
+ attribute :main_fabric do
+ api_response.&dig(:materials, 0, 0)
+ end
+
+ # ... same as before
+ end
+ end
+end
+```
+
+**Notes on attributes precedence:**
+
+- resource instance variables have the highest precedence
+- attributes from the API response take precedence over attributes from the
+ block (usually from Browser UI)
+- attributes without a value will raise a `QA::Resource::Base::NoValueError` error
+
+## Creating resources in your tests
+
+To create a resource in your tests, you can call the `.fabricate!` method on
+the resource class.
+Note that if the resource class supports API fabrication, this will use this
+fabrication by default.
+
+Here is an example that will use the API fabrication method under the hood
+since it's supported by the `Shirt` resource class:
+
+```ruby
+my_shirt = Resource::Shirt.fabricate! do |shirt|
+ shirt.name = 'my-shirt'
+end
+
+expect(page).to have_text(my_shirt.name) # => "my-shirt" from the resource's instance variable
+expect(page).to have_text(my_shirt.brand) # => "a-brand-new-brand" from the API response
+expect(page).to have_text(my_shirt.style) # => "t-shirt" from the API response
+expect(page).to have_text(my_shirt.main_fabric) # => "cotton" from the API response via the block
+```
+
+If you explicitly want to use the Browser UI fabrication method, you can call
+the `.fabricate_via_browser_ui!` method instead:
+
+```ruby
+my_shirt = Resource::Shirt.fabricate_via_browser_ui! do |shirt|
+ shirt.name = 'my-shirt'
+end
+
+expect(page).to have_text(my_shirt.name) # => "my-shirt" from the resource's instance variable
+expect(page).to have_text(my_shirt.brand) # => the brand name fetched from the `Page::Shirt::Show` page via the block
+expect(page).to have_text(my_shirt.style) # => QA::Resource::Base::NoValueError will be raised because no API response nor a block is provided
+expect(page).to have_text(my_shirt.main_fabric) # => QA::Resource::Base::NoValueError will be raised because no API response and the block didn't provide a value (because it's also based on the API response)
+```
+
+You can also explicitly use the API fabrication method, by calling the
+`.fabricate_via_api!` method:
+
+```ruby
+my_shirt = Resource::Shirt.fabricate_via_api! do |shirt|
+ shirt.name = 'my-shirt'
+end
+```
+
+In this case, the result will be similar to calling
+`Resource::Shirt.fabricate!`.
+
+## Where to ask for help?
+
+If you need more information, ask for help on `#quality` channel on Slack
+(internal, GitLab Team only).
+
+If you are not a Team Member, and you still need help to contribute, please
+open an issue in GitLab CE issue tracker with the `~QA` label.
diff --git a/qa/qa/factory/api_fabricator.rb b/qa/qa/resource/api_fabricator.rb
index b1cfb6c9783..3762a94f312 100644
--- a/qa/qa/factory/api_fabricator.rb
+++ b/qa/qa/resource/api_fabricator.rb
@@ -5,7 +5,7 @@ require 'active_support/core_ext/object/deep_dup'
require 'capybara/dsl'
module QA
- module Factory
+ module Resource
module ApiFabricator
include Airborne
include Capybara::DSL
@@ -27,7 +27,7 @@ module QA
def fabricate_via_api!
unless api_support?
- raise NotImplementedError, "Factory #{self.class.name} does not support fabrication via the API!"
+ raise NotImplementedError, "Resource #{self.class.name} does not support fabrication via the API!"
end
resource_web_url(api_post)
@@ -52,14 +52,18 @@ module QA
end
def api_get
- url = Runtime::API::Request.new(api_client, api_get_path).url
+ process_api_response(parse_body(api_get_from(api_get_path)))
+ end
+
+ def api_get_from(get_path)
+ url = Runtime::API::Request.new(api_client, get_path).url
response = get(url)
unless response.code == HTTP_STATUS_OK
raise ResourceNotFoundError, "Resource at #{url} could not be found (#{response.code}): `#{response}`."
end
- process_api_response(parse_body(response))
+ response
end
def api_post
@@ -89,8 +93,8 @@ module QA
self.api_resource = transform_api_resource(parsed_response.deep_dup)
end
- def transform_api_resource(resource)
- resource
+ def transform_api_resource(api_resource)
+ api_resource
end
end
end
diff --git a/qa/qa/factory/base.rb b/qa/qa/resource/base.rb
index e82e16f9415..f3eefb70520 100644
--- a/qa/qa/factory/base.rb
+++ b/qa/qa/resource/base.rb
@@ -4,7 +4,7 @@ require 'forwardable'
require 'capybara/dsl'
module QA
- module Factory
+ module Resource
class Base
extend SingleForwardable
include ApiFabricator
@@ -22,12 +22,16 @@ module QA
visit(web_url)
end
+ def populate(*attributes)
+ attributes.each(&method(:public_send))
+ end
+
private
def populate_attribute(name, block)
value = attribute_value(name, block)
- raise NoValueError, "No value was computed for product #{name} of factory #{self.class.name}." unless value
+ raise NoValueError, "No value was computed for #{name} of #{self.class.name}." unless value
value
end
@@ -54,11 +58,11 @@ module QA
def self.fabricate_via_browser_ui!(*args, &prepare_block)
options = args.extract_options!
- factory = options.fetch(:factory) { new }
+ resource = options.fetch(:resource) { new }
parents = options.fetch(:parents) { [] }
- do_fabricate!(factory: factory, prepare_block: prepare_block, parents: parents) do
- log_fabrication(:browser_ui, factory, parents, args) { factory.fabricate!(*args) }
+ do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do
+ log_fabrication(:browser_ui, resource, parents, args) { resource.fabricate!(*args) }
current_url
end
@@ -66,29 +70,29 @@ module QA
def self.fabricate_via_api!(*args, &prepare_block)
options = args.extract_options!
- factory = options.fetch(:factory) { new }
+ resource = options.fetch(:resource) { new }
parents = options.fetch(:parents) { [] }
- raise NotImplementedError unless factory.api_support?
+ raise NotImplementedError unless resource.api_support?
- factory.eager_load_api_client!
+ resource.eager_load_api_client!
- do_fabricate!(factory: factory, prepare_block: prepare_block, parents: parents) do
- log_fabrication(:api, factory, parents, args) { factory.fabricate_via_api! }
+ do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do
+ log_fabrication(:api, resource, parents, args) { resource.fabricate_via_api! }
end
end
- def self.do_fabricate!(factory:, prepare_block:, parents: [])
- prepare_block.call(factory) if prepare_block
+ def self.do_fabricate!(resource:, prepare_block:, parents: [])
+ prepare_block.call(resource) if prepare_block
resource_web_url = yield
- factory.web_url = resource_web_url
+ resource.web_url = resource_web_url
- Factory::Product.new(factory)
+ resource
end
private_class_method :do_fabricate!
- def self.log_fabrication(method, factory, parents, args)
+ def self.log_fabrication(method, resource, parents, args)
return yield unless Runtime::Env.debug?
start = Time.now
@@ -96,7 +100,7 @@ module QA
msg = [prefix]
msg << "Built a #{name}"
msg << "as a dependency of #{parents.last}" if parents.any?
- msg << "via #{method} with args #{args}"
+ msg << "via #{method}"
yield.tap do
msg << "in #{Time.now - start} seconds"
@@ -107,7 +111,7 @@ module QA
private_class_method :log_fabrication
def self.evaluator
- @evaluator ||= Factory::Base::DSL.new(self)
+ @evaluator ||= Base::DSL.new(self)
end
private_class_method :evaluator
diff --git a/qa/qa/resource/branch.rb b/qa/qa/resource/branch.rb
new file mode 100644
index 00000000000..bd52c4abe02
--- /dev/null
+++ b/qa/qa/resource/branch.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class Branch < Base
+ attr_accessor :project, :branch_name,
+ :allow_to_push, :allow_to_merge, :protected
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'protected-branch-project'
+ end
+ end
+
+ def initialize
+ @branch_name = 'test/branch'
+ @allow_to_push = true
+ @allow_to_merge = true
+ @protected = false
+ end
+
+ def fabricate!
+ project.visit!
+
+ Repository::ProjectPush.fabricate! do |resource|
+ resource.project = project
+ resource.file_name = 'kick-off.txt'
+ resource.commit_message = 'First commit'
+ end
+
+ branch = Repository::ProjectPush.fabricate! do |resource|
+ resource.project = project
+ resource.file_name = 'README.md'
+ resource.commit_message = 'Add readme'
+ resource.branch_name = 'master'
+ resource.new_branch = false
+ resource.remote_branch = @branch_name
+ end
+
+ Page::Project::Show.perform do |page|
+ page.wait { page.has_content?(branch_name) }
+ end
+
+ # The upcoming process will make it access the Protected Branches page,
+ # select the already created branch and protect it according
+ # to `allow_to_push` variable.
+ return branch unless @protected
+
+ Page::Project::Menu.perform(&:click_repository_settings)
+
+ Page::Project::Settings::Repository.perform do |setting|
+ setting.expand_protected_branches do |page|
+ page.select_branch(branch_name)
+
+ if allow_to_push
+ page.allow_devs_and_maintainers_to_push
+ else
+ page.allow_no_one_to_push
+ end
+
+ if allow_to_merge
+ page.allow_devs_and_maintainers_to_merge
+ else
+ page.allow_no_one_to_merge
+ end
+
+ page.wait(reload: false) do
+ !page.first('.btn-success').disabled?
+ end
+
+ page.protect_branch
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/ci_variable.rb b/qa/qa/resource/ci_variable.rb
new file mode 100644
index 00000000000..0570c47d41c
--- /dev/null
+++ b/qa/qa/resource/ci_variable.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class CiVariable < Base
+ attr_accessor :key, :value
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-with-ci-variables'
+ resource.description = 'project for adding CI variable test'
+ end
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Menu.perform(&:click_ci_cd_settings)
+
+ Page::Project::Settings::CICD.perform do |setting|
+ setting.expand_ci_variables do |page|
+ page.fill_variable(key, value)
+
+ page.save_variables
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/deploy_key.rb b/qa/qa/resource/deploy_key.rb
new file mode 100644
index 00000000000..9ed8fb7726e
--- /dev/null
+++ b/qa/qa/resource/deploy_key.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class DeployKey < Base
+ attr_accessor :title, :key
+
+ attribute :fingerprint do
+ Page::Project::Settings::Repository.perform do |setting|
+ setting.expand_deploy_keys do |key|
+ key_offset = key.key_titles.index do |key_title|
+ key_title.text == title
+ end
+
+ key.key_fingerprints[key_offset].text
+ end
+ end
+ end
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-to-deploy'
+ resource.description = 'project for adding deploy key test'
+ end
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Menu.perform(&:click_repository_settings)
+
+ Page::Project::Settings::Repository.perform do |setting|
+ setting.expand_deploy_keys do |page|
+ page.fill_key_title(title)
+ page.fill_key_value(key)
+
+ page.add_key
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/deploy_token.rb b/qa/qa/resource/deploy_token.rb
new file mode 100644
index 00000000000..cee4422f6b4
--- /dev/null
+++ b/qa/qa/resource/deploy_token.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class DeployToken < Base
+ attr_accessor :name, :expires_at
+
+ attribute :username do
+ Page::Project::Settings::Repository.perform do |page|
+ page.expand_deploy_tokens do |token|
+ token.token_username
+ end
+ end
+ end
+
+ attribute :password do
+ Page::Project::Settings::Repository.perform do |page|
+ page.expand_deploy_tokens do |token|
+ token.token_password
+ end
+ end
+ end
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-to-deploy'
+ resource.description = 'project for adding deploy token test'
+ end
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Menu.act do
+ click_repository_settings
+ end
+
+ Page::Project::Settings::Repository.perform do |setting|
+ setting.expand_deploy_tokens do |page|
+ page.fill_token_name(name)
+ page.fill_token_expires_at(expires_at)
+ page.fill_scopes(read_repository: true, read_registry: false)
+
+ page.add_token
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/file.rb b/qa/qa/resource/file.rb
new file mode 100644
index 00000000000..effc5a7940b
--- /dev/null
+++ b/qa/qa/resource/file.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class File < Base
+ attr_accessor :name,
+ :content,
+ :commit_message
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-with-new-file'
+ end
+ end
+
+ def initialize
+ @name = 'QA Test - File name'
+ @content = 'QA Test - File content'
+ @commit_message = 'QA Test - Commit message'
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Show.perform(&:create_new_file!)
+
+ Page::File::Form.perform do |page|
+ page.add_name(@name)
+ page.add_content(@content)
+ page.add_commit_message(@commit_message)
+ page.commit_changes
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/fork.rb b/qa/qa/resource/fork.rb
new file mode 100644
index 00000000000..9fd66f3a36a
--- /dev/null
+++ b/qa/qa/resource/fork.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class Fork < Base
+ attribute :push do
+ Repository::ProjectPush.fabricate!
+ end
+
+ attribute :user do
+ User.fabricate! do |resource|
+ if Runtime::Env.forker?
+ resource.username = Runtime::Env.forker_username
+ resource.password = Runtime::Env.forker_password
+ end
+ end
+ end
+
+ def fabricate!
+ populate(:push, :user)
+
+ # Sign out as admin and sign is as the fork user
+ Page::Main::Menu.perform(&:sign_out)
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform do |login|
+ login.sign_in_using_credentials(user)
+ end
+
+ push.project.visit!
+
+ Page::Project::Show.perform(&:fork_project)
+
+ Page::Project::Fork::New.perform do |fork_new|
+ fork_new.choose_namespace(user.name)
+ end
+
+ Page::Layout::Banner.perform do |page|
+ page.has_notice?('The project was successfully forked.')
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/group.rb b/qa/qa/resource/group.rb
new file mode 100644
index 00000000000..dce15e4f10b
--- /dev/null
+++ b/qa/qa/resource/group.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class Group < Base
+ attr_accessor :path, :description
+
+ attribute :sandbox do
+ Sandbox.fabricate!
+ end
+
+ attribute :id
+
+ def initialize
+ @path = Runtime::Namespace.name
+ @description = "QA test run at #{Runtime::Namespace.time}"
+ end
+
+ def fabricate!
+ sandbox.visit!
+
+ Page::Group::Show.perform do |group_show|
+ if group_show.has_subgroup?(path)
+ group_show.go_to_subgroup(path)
+ else
+ group_show.go_to_new_subgroup
+
+ Page::Group::New.perform do |group_new|
+ group_new.set_path(path)
+ group_new.set_description(description)
+ group_new.set_visibility('Public')
+ group_new.create
+ end
+
+ # Ensure that the group was actually created
+ group_show.wait(time: 1) do
+ group_show.has_text?(path) &&
+ group_show.has_new_project_or_subgroup_dropdown?
+ end
+ end
+ end
+ end
+
+ def fabricate_via_api!
+ resource_web_url(api_get)
+ rescue ResourceNotFoundError
+ super
+ end
+
+ def api_get_path
+ "/groups/#{CGI.escape("#{sandbox.path}/#{path}")}"
+ end
+
+ def api_post_path
+ '/groups'
+ end
+
+ def api_post_body
+ {
+ parent_id: sandbox.id,
+ path: path,
+ name: path,
+ visibility: 'public'
+ }
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/issue.rb b/qa/qa/resource/issue.rb
new file mode 100644
index 00000000000..2c2f27fe231
--- /dev/null
+++ b/qa/qa/resource/issue.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class Issue < Base
+ attr_writer :description
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-for-issues'
+ resource.description = 'project for adding issues'
+ end
+ end
+
+ attribute :title
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Show.perform(&:go_to_new_issue)
+
+ Page::Project::Issue::New.perform do |page|
+ page.add_title(@title)
+ page.add_description(@description)
+ page.create_new_issue
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/kubernetes_cluster.rb b/qa/qa/resource/kubernetes_cluster.rb
new file mode 100644
index 00000000000..96c8843fb99
--- /dev/null
+++ b/qa/qa/resource/kubernetes_cluster.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'securerandom'
+
+module QA
+ module Resource
+ class KubernetesCluster < Base
+ attr_writer :project, :cluster,
+ :install_helm_tiller, :install_ingress, :install_prometheus, :install_runner
+
+ attribute :ingress_ip do
+ Page::Project::Operations::Kubernetes::Show.perform(&:ingress_ip)
+ end
+
+ def fabricate!
+ @project.visit!
+
+ Page::Project::Menu.perform(
+ &:click_operations_kubernetes)
+
+ Page::Project::Operations::Kubernetes::Index.perform(
+ &:add_kubernetes_cluster)
+
+ Page::Project::Operations::Kubernetes::Add.perform(
+ &:add_existing_cluster)
+
+ Page::Project::Operations::Kubernetes::AddExisting.perform do |page|
+ page.set_cluster_name(@cluster.cluster_name)
+ page.set_api_url(@cluster.api_url)
+ page.set_ca_certificate(@cluster.ca_certificate)
+ page.set_token(@cluster.token)
+ page.check_rbac! if @cluster.rbac
+ page.add_cluster!
+ end
+
+ if @install_helm_tiller
+ Page::Project::Operations::Kubernetes::Show.perform do |page|
+ # We must wait a few seconds for permissions to be set up correctly for new cluster
+ sleep 10
+
+ # Helm must be installed before everything else
+ page.install!(:helm)
+ page.await_installed(:helm)
+
+ page.install!(:ingress) if @install_ingress
+ page.install!(:prometheus) if @install_prometheus
+ page.install!(:runner) if @install_runner
+
+ page.await_installed(:ingress) if @install_ingress
+ page.await_installed(:prometheus) if @install_prometheus
+ page.await_installed(:runner) if @install_runner
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/label.rb b/qa/qa/resource/label.rb
new file mode 100644
index 00000000000..c0869cb1f2a
--- /dev/null
+++ b/qa/qa/resource/label.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'securerandom'
+
+module QA
+ module Resource
+ class Label < Base
+ attr_accessor :description, :color
+
+ attribute :title
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-with-label'
+ end
+ end
+
+ def initialize
+ @title = "qa-test-#{SecureRandom.hex(8)}"
+ @description = 'This is a test label'
+ @color = '#0033CC'
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Menu.perform(&:go_to_labels)
+ Page::Label::Index.perform(&:go_to_new_label)
+
+ Page::Label::New.perform do |page|
+ page.fill_title(@title)
+ page.fill_description(@description)
+ page.fill_color(@color)
+ page.create_label
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/merge_request.rb b/qa/qa/resource/merge_request.rb
new file mode 100644
index 00000000000..77afb3cfcba
--- /dev/null
+++ b/qa/qa/resource/merge_request.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require 'securerandom'
+
+module QA
+ module Resource
+ class MergeRequest < Base
+ attr_accessor :title,
+ :description,
+ :source_branch,
+ :target_branch,
+ :assignee,
+ :milestone,
+ :labels,
+ :file_name,
+ :file_content
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-with-merge-request'
+ end
+ end
+
+ attribute :target do
+ project.visit!
+
+ Repository::ProjectPush.fabricate! do |resource|
+ resource.project = project
+ resource.branch_name = 'master'
+ resource.remote_branch = target_branch
+ end
+ end
+
+ attribute :source do
+ Repository::ProjectPush.fabricate! do |resource|
+ resource.project = project
+ resource.branch_name = target_branch
+ resource.remote_branch = source_branch
+ resource.new_branch = false
+ resource.file_name = file_name
+ resource.file_content = file_content
+ end
+ end
+
+ def initialize
+ @title = 'QA test - merge request'
+ @description = 'This is a test merge request'
+ @source_branch = "qa-test-feature-#{SecureRandom.hex(8)}"
+ @target_branch = "master"
+ @assignee = nil
+ @milestone = nil
+ @labels = []
+ @file_name = "added_file.txt"
+ @file_content = "File Added"
+ end
+
+ def fabricate!
+ populate(:target, :source)
+
+ project.visit!
+ Page::Project::Show.perform(&:new_merge_request)
+ Page::MergeRequest::New.perform do |page|
+ page.fill_title(@title)
+ page.fill_description(@description)
+ page.choose_milestone(@milestone) if @milestone
+ labels.each do |label|
+ page.select_label(label)
+ end
+
+ page.create_merge_request
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/merge_request_from_fork.rb b/qa/qa/resource/merge_request_from_fork.rb
new file mode 100644
index 00000000000..f91ae299d76
--- /dev/null
+++ b/qa/qa/resource/merge_request_from_fork.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class MergeRequestFromFork < MergeRequest
+ attr_accessor :fork_branch
+
+ attribute :fork do
+ Fork.fabricate!
+ end
+
+ attribute :push do
+ Repository::ProjectPush.fabricate! do |resource|
+ resource.project = fork
+ resource.branch_name = fork_branch
+ resource.file_name = 'file2.txt'
+ resource.user = fork.user
+ end
+ end
+
+ def fabricate!
+ populate(:push)
+
+ fork.visit!
+
+ Page::Project::Show.perform(&:new_merge_request)
+ Page::MergeRequest::New.perform(&:create_merge_request)
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/personal_access_token.rb b/qa/qa/resource/personal_access_token.rb
new file mode 100644
index 00000000000..b8dd0a3562f
--- /dev/null
+++ b/qa/qa/resource/personal_access_token.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ ##
+ # Create a personal access token that can be used by the api
+ #
+ class PersonalAccessToken < Base
+ attr_accessor :name
+
+ attribute :access_token do
+ Page::Profile::PersonalAccessTokens.perform(&:created_access_token)
+ end
+
+ def fabricate!
+ Page::Main::Menu.perform(&:go_to_profile_settings)
+ Page::Profile::Menu.perform(&:click_access_tokens)
+
+ Page::Profile::PersonalAccessTokens.perform do |page|
+ page.fill_token_name(name || 'api-test-token')
+ page.check_api
+ page.create_token
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb
new file mode 100644
index 00000000000..7fdf69278f9
--- /dev/null
+++ b/qa/qa/resource/project.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'securerandom'
+
+module QA
+ module Resource
+ class Project < Base
+ attribute :name
+ attribute :description
+
+ attribute :group do
+ Group.fabricate!
+ end
+
+ attribute :repository_ssh_location do
+ Page::Project::Show.perform do |page|
+ page.choose_repository_clone_ssh
+ page.repository_location
+ end
+ end
+
+ attribute :repository_http_location do
+ Page::Project::Show.perform do |page|
+ page.choose_repository_clone_http
+ page.repository_location
+ end
+ end
+
+ def initialize
+ @description = 'My awesome project'
+ end
+
+ def name=(raw_name)
+ @name = "#{raw_name}-#{SecureRandom.hex(8)}"
+ end
+
+ def fabricate!
+ group.visit!
+
+ Page::Group::Show.perform(&:go_to_new_project)
+
+ Page::Project::New.perform do |page|
+ page.choose_test_namespace
+ page.choose_name(@name)
+ page.add_description(@description)
+ page.set_visibility('Public')
+ page.create_new_project
+ end
+ end
+
+ def api_get_path
+ "/projects/#{name}"
+ end
+
+ def api_post_path
+ '/projects'
+ end
+
+ def api_post_body
+ {
+ namespace_id: group.id,
+ path: name,
+ name: name,
+ description: description,
+ visibility: 'public'
+ }
+ end
+
+ private
+
+ def transform_api_resource(api_resource)
+ api_resource[:repository_ssh_location] =
+ Git::Location.new(api_resource[:ssh_url_to_repo])
+ api_resource[:repository_http_location] =
+ Git::Location.new(api_resource[:http_url_to_repo])
+ api_resource
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/project_imported_from_github.rb b/qa/qa/resource/project_imported_from_github.rb
new file mode 100644
index 00000000000..3f02fe885a9
--- /dev/null
+++ b/qa/qa/resource/project_imported_from_github.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'securerandom'
+
+module QA
+ module Resource
+ class ProjectImportedFromGithub < Project
+ attr_accessor :name
+ attr_writer :personal_access_token, :github_repository_path
+
+ attribute :group do
+ Group.fabricate!
+ end
+
+ def fabricate!
+ group.visit!
+
+ Page::Group::Show.perform(&:go_to_new_project)
+
+ Page::Project::New.perform do |page|
+ page.go_to_import_project
+ end
+
+ Page::Project::New.perform do |page|
+ page.go_to_github_import
+ end
+
+ Page::Project::Import::Github.perform do |page|
+ page.add_personal_access_token(@personal_access_token)
+ page.list_repos
+ page.import!(@github_repository_path, @name)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/project_milestone.rb b/qa/qa/resource/project_milestone.rb
new file mode 100644
index 00000000000..a4d6657caff
--- /dev/null
+++ b/qa/qa/resource/project_milestone.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class ProjectMilestone < Base
+ attr_reader :title
+ attr_accessor :description
+
+ attribute :project do
+ Project.fabricate!
+ end
+
+ def title=(title)
+ @title = "#{title}-#{SecureRandom.hex(4)}"
+ @description = 'A milestone'
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Menu.perform do |page|
+ page.click_issues
+ page.click_milestones
+ end
+
+ Page::Project::Milestone::Index.perform(&:click_new_milestone)
+
+ Page::Project::Milestone::New.perform do |milestone_new|
+ milestone_new.set_title(@title)
+ milestone_new.set_description(@description)
+ milestone_new.create_new_milestone
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/factory/repository/project_push.rb b/qa/qa/resource/repository/project_push.rb
index a9dfbc0a783..c9fafe3419f 100644
--- a/qa/qa/factory/repository/project_push.rb
+++ b/qa/qa/resource/repository/project_push.rb
@@ -1,16 +1,16 @@
+# frozen_string_literal: true
+
module QA
- module Factory
+ module Resource
module Repository
- class ProjectPush < Factory::Repository::Push
+ class ProjectPush < Repository::Push
attribute :project do
- Factory::Resource::Project.fabricate! do |resource|
+ Project.fabricate! do |resource|
resource.name = 'project-with-code'
resource.description = 'Project with repository'
end
end
- attribute :output
-
def initialize
@file_name = 'file.txt'
@file_content = '# This is test project'
diff --git a/qa/qa/factory/repository/push.rb b/qa/qa/resource/repository/push.rb
index ffa755b9e88..c14d97ff7fb 100644
--- a/qa/qa/factory/repository/push.rb
+++ b/qa/qa/resource/repository/push.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
require 'pathname'
module QA
- module Factory
+ module Resource
module Repository
- class Push < Factory::Base
+ class Push < Base
attr_accessor :file_name, :file_content, :commit_message,
:branch_name, :new_branch, :output, :repository_http_uri,
:repository_ssh_uri, :ssh_key, :user
diff --git a/qa/qa/factory/repository/wiki_push.rb b/qa/qa/resource/repository/wiki_push.rb
index 25b6ffe8323..f1c39d507fe 100644
--- a/qa/qa/factory/repository/wiki_push.rb
+++ b/qa/qa/resource/repository/wiki_push.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
module QA
- module Factory
+ module Resource
module Repository
- class WikiPush < Factory::Repository::Push
+ class WikiPush < Repository::Push
attribute :wiki do
- Factory::Resource::Wiki.fabricate! do |resource|
+ Wiki.fabricate! do |resource|
resource.title = 'Home'
resource.content = '# My First Wiki Content'
resource.message = 'Update home'
diff --git a/qa/qa/resource/runner.rb b/qa/qa/resource/runner.rb
new file mode 100644
index 00000000000..08ae3f22117
--- /dev/null
+++ b/qa/qa/resource/runner.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'securerandom'
+
+module QA
+ module Resource
+ class Runner < Base
+ attr_writer :name, :tags, :image
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-with-ci-cd'
+ resource.description = 'Project with CI/CD Pipelines'
+ end
+ end
+
+ def name
+ @name || "qa-runner-#{SecureRandom.hex(4)}"
+ end
+
+ def tags
+ @tags || %w[qa e2e]
+ end
+
+ def image
+ @image || 'gitlab/gitlab-runner:alpine'
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Menu.perform(&:click_ci_cd_settings)
+
+ Service::Runner.new(name).tap do |runner|
+ Page::Project::Settings::CICD.perform do |settings|
+ settings.expand_runners_settings do |runners|
+ runner.pull
+ runner.token = runners.registration_token
+ runner.address = runners.coordinator_address
+ runner.tags = tags
+ runner.image = image
+ runner.register!
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/sandbox.rb b/qa/qa/resource/sandbox.rb
new file mode 100644
index 00000000000..41ce857a8b8
--- /dev/null
+++ b/qa/qa/resource/sandbox.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ ##
+ # Ensure we're in our sandbox namespace, either by navigating to it or by
+ # creating it if it doesn't yet exist.
+ #
+ class Sandbox < Base
+ attr_reader :path
+
+ attribute :id
+
+ def initialize
+ @path = Runtime::Namespace.sandbox_name
+ end
+
+ def fabricate!
+ Page::Main::Menu.perform(&:go_to_groups)
+
+ Page::Dashboard::Groups.perform do |page|
+ if page.has_group?(path)
+ page.go_to_group(path)
+ else
+ page.go_to_new_group
+
+ Page::Group::New.perform do |group|
+ group.set_path(path)
+ group.set_description('GitLab QA Sandbox Group')
+ group.set_visibility('Public')
+ group.create
+ end
+ end
+ end
+ end
+
+ def fabricate_via_api!
+ resource_web_url(api_get)
+ rescue ResourceNotFoundError
+ super
+ end
+
+ def api_get_path
+ "/groups/#{path}"
+ end
+
+ def api_post_path
+ '/groups'
+ end
+
+ def api_post_body
+ {
+ path: path,
+ name: path,
+ visibility: 'public'
+ }
+ end
+ end
+ end
+end
diff --git a/qa/qa/factory/settings/hashed_storage.rb b/qa/qa/resource/settings/hashed_storage.rb
index 4e32382f910..40c06768ffe 100644
--- a/qa/qa/factory/settings/hashed_storage.rb
+++ b/qa/qa/resource/settings/hashed_storage.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
module QA
- module Factory
+ module Resource
module Settings
- class HashedStorage < Factory::Base
+ class HashedStorage < Base
def fabricate!(*traits)
raise ArgumentError unless traits.include?(:enabled)
diff --git a/qa/qa/resource/ssh_key.rb b/qa/qa/resource/ssh_key.rb
new file mode 100644
index 00000000000..c6c97c8532f
--- /dev/null
+++ b/qa/qa/resource/ssh_key.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class SSHKey < Base
+ extend Forwardable
+
+ attr_accessor :title
+
+ def_delegators :key, :private_key, :public_key, :fingerprint
+
+ def key
+ @key ||= Runtime::Key::RSA.new
+ end
+
+ def fabricate!
+ Page::Main::Menu.perform(&:go_to_profile_settings)
+ Page::Profile::Menu.perform(&:click_ssh_keys)
+
+ Page::Profile::SSHKeys.perform do |page|
+ page.add_key(public_key, title)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/user.rb b/qa/qa/resource/user.rb
new file mode 100644
index 00000000000..16f0b311fa9
--- /dev/null
+++ b/qa/qa/resource/user.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'securerandom'
+
+module QA
+ module Resource
+ class User < Base
+ attr_reader :unique_id
+ attr_writer :username, :password
+
+ def initialize
+ @unique_id = SecureRandom.hex(8)
+ end
+
+ def username
+ @username ||= "qa-user-#{unique_id}"
+ end
+
+ def password
+ @password ||= 'password'
+ end
+
+ def name
+ @name ||= username
+ end
+
+ def email
+ @email ||= "#{username}@example.com"
+ end
+
+ def credentials_given?
+ defined?(@username) && defined?(@password)
+ end
+
+ def fabricate!
+ # Don't try to log-out if we're not logged-in
+ if Page::Main::Menu.perform { |p| p.has_personal_area?(wait: 0) }
+ Page::Main::Menu.perform { |main| main.sign_out }
+ end
+
+ if credentials_given?
+ Page::Main::Login.perform do |login|
+ login.sign_in_using_credentials(self)
+ end
+ else
+ Page::Main::Login.perform do |login|
+ login.switch_to_register_tab
+ end
+ Page::Main::SignUp.perform do |signup|
+ signup.sign_up!(self)
+ end
+ end
+ end
+
+ def fabricate_via_api!
+ resource_web_url(api_get)
+ rescue ResourceNotFoundError
+ super
+ end
+
+ def api_get_path
+ "/users/#{fetch_id(username)}"
+ end
+
+ def api_post_path
+ '/users'
+ end
+
+ def api_post_body
+ {
+ email: email,
+ password: password,
+ username: username,
+ name: name,
+ skip_confirmation: true
+ }
+ end
+
+ private
+
+ def fetch_id(username)
+ users = parse_body(api_get_from("/users?username=#{username}"))
+
+ unless users.size == 1 && users.first[:username] == username
+ raise ResourceNotFoundError, "Expected one user with username #{username} but found: `#{users}`."
+ end
+
+ users.first[:id]
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/wiki.rb b/qa/qa/resource/wiki.rb
new file mode 100644
index 00000000000..e942e9718a0
--- /dev/null
+++ b/qa/qa/resource/wiki.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class Wiki < Base
+ attr_accessor :title, :content, :message
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-for-wikis'
+ resource.description = 'project for adding wikis'
+ end
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Menu.perform { |menu_side| menu_side.click_wiki }
+
+ Page::Project::Wiki::New.perform do |wiki_new|
+ wiki_new.go_to_create_first_page
+ wiki_new.set_title(@title)
+ wiki_new.set_content(@content)
+ wiki_new.set_message(@message)
+ wiki_new.create_new_page
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/api/client.rb b/qa/qa/runtime/api/client.rb
index 0545b500e4c..aff84c89f0e 100644
--- a/qa/qa/runtime/api/client.rb
+++ b/qa/qa/runtime/api/client.rb
@@ -32,7 +32,7 @@ module QA
def do_create_personal_access_token
Page::Main::Login.act { sign_in_using_credentials }
- Factory::Resource::PersonalAccessToken.fabricate!.access_token
+ Resource::PersonalAccessToken.fabricate!.access_token
end
end
end
diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb
index c7052a9f300..c4500f9be90 100644
--- a/qa/qa/runtime/env.rb
+++ b/qa/qa/runtime/env.rb
@@ -7,6 +7,16 @@ module QA
attr_writer :personal_access_token
+ # The environment variables used to indicate if the environment under test
+ # supports the given feature
+ SUPPORTED_FEATURES = {
+ git_protocol_v2: 'QA_CAN_TEST_GIT_PROTOCOL_V2'
+ }.freeze
+
+ def supported_features
+ SUPPORTED_FEATURES
+ end
+
def debug?
enabled?(ENV['QA_DEBUG'], default: false)
end
@@ -104,6 +114,15 @@ module QA
raise ArgumentError, "Please provide GITHUB_ACCESS_TOKEN"
end
+ # Returns true if there is an environment variable that indicates that
+ # the feature is supported in the environment under test.
+ # All features are supported by default.
+ def can_test?(feature)
+ raise ArgumentError, %Q(Unknown feature "#{feature}") unless SUPPORTED_FEATURES.include? feature
+
+ enabled?(ENV[SUPPORTED_FEATURES[feature]], default: true)
+ end
+
private
def enabled?(value, default: true)
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 44071ec3e45..185837edacf 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
@@ -5,7 +5,7 @@ module QA
it 'user registers and logs in' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Factory::Resource::User.fabricate!
+ 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.
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb
index 7bf26c22fa6..bef89d5be24 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb
@@ -5,18 +5,16 @@ module QA
describe 'Add project member' do
it 'user adds project member' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform(&:sign_in_using_credentials)
- user = Factory::Resource::User.fabricate!
+ user = Resource::User.fabricate!
- Page::Main::Menu.perform { |main| main.sign_out }
- Page::Main::Login.act { sign_in_using_credentials }
-
- project = Factory::Resource::Project.fabricate! do |resource|
+ project = Resource::Project.fabricate! do |resource|
resource.name = 'add-member-project'
end
project.visit!
- Page::Project::Menu.act { click_members_settings }
+ Page::Project::Menu.perform(&:click_members_settings)
Page::Project::Settings::Members.perform do |page|
page.add_member(user.username)
end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb
index a242f2158da..6632c2977ef 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb
@@ -7,7 +7,7 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- created_project = Factory::Resource::Project.fabricate_via_browser_ui! do |project|
+ created_project = Resource::Project.fabricate_via_browser_ui! do |project|
project.name = 'awesome-project'
project.description = 'create awesome project test'
end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb
index a99b0522e73..3ce48de2c25 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb
@@ -4,7 +4,7 @@ module QA
context 'Manage', :orchestrated, :github do
describe 'Project import from GitHub' do
let(:imported_project) do
- Factory::Resource::ProjectImportedFromGithub.fabricate! do |project|
+ Resource::ProjectImportedFromGithub.fabricate! do |project|
project.name = 'imported-project'
project.personal_access_token = Runtime::Env.github_access_token
project.github_repository_path = 'gitlab-qa/test-project'
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb
index 768d40f3acf..275de3d332c 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb
@@ -7,7 +7,7 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Factory::Repository::ProjectPush.fabricate! do |push|
+ Resource::Repository::ProjectPush.fabricate! do |push|
push.file_name = 'README.md'
push.file_content = '# This is a test project'
push.commit_message = 'Add README.md'
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb
index e67561b3a39..7145b950b6c 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb
@@ -9,7 +9,7 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Factory::Resource::Issue.fabricate! do |issue|
+ Resource::Issue.fabricate! do |issue|
issue.title = issue_title
end
end
@@ -31,6 +31,7 @@ module QA
create_issue
Page::Project::Issue::Show.perform do |show|
+ show.select_all_activities_filter
show.comment('See attached banana for scale', attachment: file_to_attach)
show.refresh
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb
index 24877d937d2..ac34f72bb8f 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb
@@ -9,7 +9,7 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Factory::Resource::Issue.fabricate! do |issue|
+ Resource::Issue.fabricate! do |issue|
issue.title = issue_title
end
@@ -27,6 +27,11 @@ module QA
expect(show_page).to have_content("made the issue confidential")
expect(show_page).to have_content("My own comment")
+
+ show_page.select_history_only_filter
+
+ expect(show_page).to have_content("made the issue confidential")
+ expect(show_page).not_to have_content("My own comment")
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb
index 037ff5efbd4..d33947f41da 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb
@@ -7,22 +7,22 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- current_project = Factory::Resource::Project.fabricate! do |project|
+ current_project = Resource::Project.fabricate! do |project|
project.name = 'project-with-merge-request-and-milestone'
end
- current_milestone = Factory::Resource::ProjectMilestone.fabricate! do |milestone|
+ current_milestone = Resource::ProjectMilestone.fabricate! do |milestone|
milestone.title = 'unique-milestone'
milestone.project = current_project
end
- new_label = Factory::Resource::Label.fabricate! do |label|
+ new_label = Resource::Label.fabricate! do |label|
label.project = current_project
label.title = 'qa-mr-test-label'
label.description = 'Merge Request label'
end
- Factory::Resource::MergeRequest.fabricate! do |merge_request|
+ Resource::MergeRequest.fabricate! do |merge_request|
merge_request.title = 'This is a merge request with a milestone'
merge_request.description = 'Great feature with milestone'
merge_request.project = current_project
@@ -49,11 +49,11 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- current_project = Factory::Resource::Project.fabricate! do |project|
+ current_project = Resource::Project.fabricate! do |project|
project.name = 'project-with-merge-request'
end
- Factory::Resource::MergeRequest.fabricate! do |merge_request|
+ Resource::MergeRequest.fabricate! do |merge_request|
merge_request.title = 'This is a merge request'
merge_request.description = 'Great feature'
merge_request.project = current_project
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb
index 058af8aebdd..6dcd74471fe 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb
@@ -7,7 +7,7 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- merge_request = Factory::Resource::MergeRequestFromFork.fabricate! do |merge_request|
+ merge_request = Resource::MergeRequestFromFork.fabricate! do |merge_request|
merge_request.fork_branch = 'feature-branch'
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb
index 3bcf086d332..e2d639fd150 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb
@@ -7,7 +7,7 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- project = Factory::Resource::Project.fabricate! do |project|
+ project = Resource::Project.fabricate! do |project|
project.name = "only-fast-forward"
end
project.visit!
@@ -15,12 +15,12 @@ module QA
Page::Project::Menu.act { go_to_settings }
Page::Project::Settings::MergeRequest.act { enable_ff_only }
- merge_request = Factory::Resource::MergeRequest.fabricate! do |merge_request|
+ merge_request = Resource::MergeRequest.fabricate! do |merge_request|
merge_request.project = project
merge_request.title = 'Needs rebasing'
end
- Factory::Repository::ProjectPush.fabricate! do |push|
+ Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.file_name = "other.txt"
push.file_content = "New file added!"
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb
index 724c48cd125..6ff7360c413 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb
@@ -7,16 +7,16 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- project = Factory::Resource::Project.fabricate! do |project|
+ project = Resource::Project.fabricate! do |project|
project.name = "squash-before-merge"
end
- merge_request = Factory::Resource::MergeRequest.fabricate! do |merge_request|
+ merge_request = Resource::MergeRequest.fabricate! do |merge_request|
merge_request.project = project
merge_request.title = 'Squashing commits'
end
- Factory::Repository::ProjectPush.fabricate! do |push|
+ Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.commit_message = 'to be squashed'
push.branch_name = merge_request.source_branch
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 7705e12b95e..297485dd81e 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
@@ -13,7 +13,7 @@ module QA
before(:all) do
login
- @project = Factory::Resource::Project.fabricate! do |project|
+ @project = Resource::Project.fabricate! do |project|
project.name = 'file-template-project'
project.description = 'Add file templates via the Files view'
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb
index df70b9608d9..94be66782c6 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb
@@ -9,7 +9,7 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- key = Factory::Resource::SSHKey.fabricate! do |resource|
+ key = Resource::SSHKey.fabricate! do |resource|
resource.title = key_title
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb
index b18dee53cbc..6a0add56fe0 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb
@@ -14,7 +14,7 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- project = Factory::Resource::Project.fabricate! do |scenario|
+ project = Resource::Project.fabricate! do |scenario|
scenario.name = 'project-with-code'
scenario.description = 'project for git clone tests'
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb
index f65a1569fb0..46346d1b984 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb
@@ -12,7 +12,7 @@ module QA
file_content = 'QA Test - File content'
commit_message_for_create = 'QA Test - Create new file'
- Factory::Resource::File.fabricate! do |file|
+ Resource::File.fabricate! do |file|
file.name = file_name
file.content = file_content
file.commit_message = commit_message_for_create
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb
new file mode 100644
index 00000000000..43894372cf5
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Create' do
+ describe 'Push over HTTP using Git protocol version 2', :requires_git_protocol_v2 do
+ it 'user pushes to the repository' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform(&:sign_in_using_credentials)
+
+ # Create a project to push to
+ project = Resource::Project.fabricate! do |project|
+ project.name = 'git-protocol-project'
+ end
+
+ file_name = 'README.md'
+ file_content = 'Test Git protocol v2'
+ git_protocol = '2'
+ git_protocol_reported = nil
+
+ # Use Git to clone the project, push a file to it, and then check the
+ # supported Git protocol
+ Git::Repository.perform do |repository|
+ username = 'GitLab QA'
+ email = 'root@gitlab.com'
+
+ repository.uri = project.repository_http_location.uri
+ repository.use_default_credentials
+ repository.clone
+ repository.configure_identity(username, email)
+
+ git_protocol_reported = repository.push_with_git_protocol(
+ git_protocol,
+ file_name,
+ file_content)
+ end
+
+ project.visit!
+ Page::Project::Show.perform(&:wait_for_push)
+
+ # Check that the push worked
+ expect(page).to have_content(file_name)
+ expect(page).to have_content(file_content)
+
+ # And check that the correct Git protocol was used
+ expect(git_protocol_reported).to eq(git_protocol)
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb
new file mode 100644
index 00000000000..135925c007f
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Create' do
+ describe 'Push over SSH using Git protocol version 2', :requires_git_protocol_v2 do
+ # Note: If you run this test against GDK make sure you've enabled sshd and
+ # enabled setting the Git protocol by adding `AcceptEnv GIT_PROTOCOL` to
+ # `sshd_config`
+ # See: https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/run_qa_against_gdk.md
+
+ let(:key_title) { "key for ssh tests #{Time.now.to_f}" }
+ let(:ssh_key) do
+ Resource::SSHKey.fabricate! do |resource|
+ resource.title = key_title
+ end
+ end
+
+ def login
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform(&:sign_in_using_credentials)
+ end
+
+ around do |example|
+ # Create an SSH key to be used with Git
+ login
+ ssh_key
+
+ example.run
+
+ # Remove the SSH key
+ login
+ Page::Main::Menu.perform(&:go_to_profile_settings)
+ Page::Profile::Menu.perform(&:click_ssh_keys)
+ Page::Profile::SSHKeys.perform do |ssh_keys|
+ ssh_keys.remove_key(key_title)
+ end
+ end
+
+ it 'user pushes to the repository' do
+ # Create a project to push to
+ project = Resource::Project.fabricate! do |project|
+ project.name = 'git-protocol-project'
+ end
+
+ file_name = 'README.md'
+ file_content = 'Test Git protocol v2'
+ git_protocol = '2'
+ git_protocol_reported = nil
+
+ # Use Git to clone the project, push a file to it, and then check the
+ # supported Git protocol
+ Git::Repository.perform do |repository|
+ username = 'GitLab QA'
+ email = 'root@gitlab.com'
+
+ repository.uri = project.repository_ssh_location.uri
+
+ begin
+ repository.use_ssh_key(ssh_key)
+ repository.clone
+ repository.configure_identity(username, email)
+
+ git_protocol_reported = repository.push_with_git_protocol(
+ git_protocol,
+ file_name,
+ file_content)
+ ensure
+ repository.delete_ssh_key
+ end
+ end
+
+ project.visit!
+ Page::Project::Show.perform(&:wait_for_push)
+
+ # Check that the push worked
+ expect(page).to have_content(file_name)
+ expect(page).to have_content(file_content)
+
+ # And check that the correct Git protocol was used
+ expect(git_protocol_reported).to eq(git_protocol)
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb
index 8e4210482a2..a63b7dce8d6 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb
@@ -7,14 +7,14 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.perform(&:sign_in_using_credentials)
- access_token = Factory::Resource::PersonalAccessToken.fabricate!.access_token
+ access_token = Resource::PersonalAccessToken.fabricate!.access_token
- user = Factory::Resource::User.new.tap do |user|
+ user = Resource::User.new.tap do |user|
user.username = Runtime::User.username
user.password = access_token
end
- push = Factory::Repository::ProjectPush.fabricate! do |push|
+ push = Resource::Repository::ProjectPush.fabricate! do |push|
push.user = user
push.file_name = 'README.md'
push.file_content = '# This is a test project'
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb
index 2f63a07e0c3..92f596a44d9 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb
@@ -7,7 +7,7 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Factory::Repository::ProjectPush.fabricate! do |push|
+ Resource::Repository::ProjectPush.fabricate! do |push|
push.file_name = 'README.md'
push.file_content = '# This is a test project'
push.commit_message = 'Add README.md'
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb
index ac71cf52b6f..73a3dc14a65 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb
@@ -6,7 +6,7 @@ module QA
let(:branch_name) { 'protected-branch' }
let(:commit_message) { 'Protected push commit message' }
let(:project) do
- Factory::Resource::Project.fabricate! do |resource|
+ Resource::Project.fabricate! do |resource|
resource.name = 'protected-branch-project'
end
end
@@ -47,7 +47,7 @@ module QA
end
def create_protected_branch(allow_to_push:)
- Factory::Resource::Branch.fabricate! do |resource|
+ Resource::Branch.fabricate! do |resource|
resource.branch_name = branch_name
resource.project = project
resource.allow_to_push = allow_to_push
@@ -56,7 +56,7 @@ module QA
end
def push_new_file(branch)
- Factory::Repository::ProjectPush.fabricate! do |resource|
+ Resource::Repository::ProjectPush.fabricate! do |resource|
resource.project = project
resource.file_name = 'new_file.md'
resource.file_content = '# This is a new file'
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb
index 36068ffba69..9c764424129 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb
@@ -12,11 +12,11 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- key = Factory::Resource::SSHKey.fabricate! do |resource|
+ key = Resource::SSHKey.fabricate! do |resource|
resource.title = key_title
end
- Factory::Repository::ProjectPush.fabricate! do |push|
+ Resource::Repository::ProjectPush.fabricate! do |push|
push.ssh_key = key
push.file_name = 'README.md'
push.file_content = '# Test Use SSH Key'
diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb
index 07dbf39a8a3..e7374377104 100644
--- a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb
@@ -13,7 +13,7 @@ module QA
before(:all) do
login
- @project = Factory::Resource::Project.fabricate! do |project|
+ @project = Resource::Project.fabricate! do |project|
project.name = 'file-template-project'
project.description = 'Add file templates via the Web IDE'
end
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 4126fd9fd3e..210271705d9 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
@@ -18,7 +18,7 @@ module QA
end
it 'user creates, edits, clones, and pushes to the wiki' do
- wiki = Factory::Resource::Wiki.fabricate! do |resource|
+ wiki = Resource::Wiki.fabricate! do |resource|
resource.title = 'Home'
resource.content = '# My First Wiki Content'
resource.message = 'Update home'
@@ -34,7 +34,7 @@ module QA
validate_content('My Second Wiki Content')
- Factory::Repository::WikiPush.fabricate! do |push|
+ Resource::Repository::WikiPush.fabricate! do |push|
push.wiki = wiki
push.file_name = 'Home.md'
push.file_content = '# My Third Wiki Content'
diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb
index 58b272adcf1..0837b720df1 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb
@@ -7,7 +7,7 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Factory::Resource::CiVariable.fabricate! do |resource|
+ Resource::CiVariable.fabricate! do |resource|
resource.key = 'VARIABLE_KEY'
resource.value = 'some CI variable'
end
diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb
index d66bcce879b..25cbe41c684 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb
@@ -13,18 +13,18 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- project = Factory::Resource::Project.fabricate! do |project|
+ project = Resource::Project.fabricate! do |project|
project.name = 'project-with-pipelines'
project.description = 'Project with CI/CD Pipelines.'
end
- Factory::Resource::Runner.fabricate! do |runner|
+ Resource::Runner.fabricate! do |runner|
runner.project = project
runner.name = executor
runner.tags = %w[qa test]
end
- Factory::Repository::ProjectPush.fabricate! do |push|
+ Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.file_name = '.gitlab-ci.yml'
push.commit_message = 'Add .gitlab-ci.yml'
diff --git a/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb
index 5d9aa00582f..3af7db751e7 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb
@@ -13,7 +13,7 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Factory::Resource::Runner.fabricate! do |runner|
+ Resource::Runner.fabricate! do |runner|
runner.name = executor
end
diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb
index 64b98da8bf5..84757f25379 100644
--- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb
+++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb
@@ -11,7 +11,7 @@ module QA
deploy_key_title = 'deploy key title'
deploy_key_value = key.public_key
- deploy_key = Factory::Resource::DeployKey.fabricate! do |resource|
+ deploy_key = Resource::DeployKey.fabricate! do |resource|
resource.title = deploy_key_title
resource.key = deploy_key_value
end
diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb
index 604641e54b8..e2320c92343 100644
--- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb
+++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb
@@ -15,13 +15,13 @@ module QA
@runner_name = "qa-runner-#{Time.now.to_i}"
- @project = Factory::Resource::Project.fabricate! do |resource|
+ @project = Resource::Project.fabricate! do |resource|
resource.name = 'deploy-key-clone-project'
end
@repository_location = @project.repository_ssh_location
- Factory::Resource::Runner.fabricate! do |resource|
+ Resource::Runner.fabricate! do |resource|
resource.project = @project
resource.name = @runner_name
resource.tags = %w[qa docker]
@@ -47,7 +47,7 @@ module QA
login
- Factory::Resource::DeployKey.fabricate! do |resource|
+ Resource::DeployKey.fabricate! do |resource|
resource.project = @project
resource.title = "deploy key #{key.name}(#{key.bits})"
resource.key = key.public_key
@@ -55,7 +55,7 @@ module QA
deploy_key_name = "DEPLOY_KEY_#{key.name}_#{key.bits}"
- Factory::Resource::CiVariable.fabricate! do |resource|
+ Resource::CiVariable.fabricate! do |resource|
resource.project = @project
resource.key = deploy_key_name
resource.value = key.private_key
@@ -78,7 +78,7 @@ module QA
- docker
YAML
- Factory::Repository::ProjectPush.fabricate! do |resource|
+ Resource::Repository::ProjectPush.fabricate! do |resource|
resource.project = @project
resource.file_name = '.gitlab-ci.yml'
resource.commit_message = 'Add .gitlab-ci.yml'
diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb
index 263ba6a6800..9f34e4218c1 100644
--- a/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb
+++ b/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb
@@ -10,7 +10,7 @@ module QA
deploy_token_name = 'deploy token name'
deploy_token_expires_at = Date.today + 7 # 1 Week from now
- deploy_token = Factory::Resource::DeployToken.fabricate! do |resource|
+ deploy_token = Resource::DeployToken.fabricate! do |resource|
resource.name = deploy_token_name
resource.expires_at = deploy_token_expires_at
end
diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
index c2fce1e7df1..30ec0665973 100644
--- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
+++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
@@ -15,21 +15,21 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- project = Factory::Resource::Project.fabricate! do |p|
+ project = Resource::Project.fabricate! do |p|
p.name = 'project-with-autodevops'
p.description = 'Project with Auto Devops'
end
# Disable code_quality check in Auto DevOps pipeline as it takes
# too long and times out the test
- Factory::Resource::CiVariable.fabricate! do |resource|
+ Resource::CiVariable.fabricate! do |resource|
resource.project = project
resource.key = 'CODE_QUALITY_DISABLED'
resource.value = '1'
end
# Create Auto Devops compatible repo
- Factory::Repository::ProjectPush.fabricate! do |push|
+ Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.directory = Pathname
.new(__dir__)
@@ -41,7 +41,7 @@ module QA
# Create and connect K8s cluster
@cluster = Service::KubernetesCluster.new(rbac: rbac).create!
- kubernetes_cluster = Factory::Resource::KubernetesCluster.fabricate! do |cluster|
+ kubernetes_cluster = Resource::KubernetesCluster.fabricate! do |cluster|
cluster.project = project
cluster.cluster = @cluster
cluster.install_helm_tiller = true
diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb
index ad397c13f0c..1bd8101c36d 100644
--- a/qa/qa/specs/runner.rb
+++ b/qa/qa/specs/runner.rb
@@ -25,6 +25,10 @@ module QA
args.push(%w[--tag ~skip_signup_disabled]) if QA::Runtime::Env.signup_disabled?
+ QA::Runtime::Env.supported_features.each_key do |key|
+ args.push(["--tag", "~requires_#{key}"]) unless QA::Runtime::Env.can_test? key
+ end
+
args.push(options)
args.push(DEFAULT_TEST_PATH_ARGS) unless options.any? { |opt| opt =~ %r{/features/} }
diff --git a/qa/spec/factory/base_spec.rb b/qa/spec/factory/base_spec.rb
deleted file mode 100644
index d7b92052894..00000000000
--- a/qa/spec/factory/base_spec.rb
+++ /dev/null
@@ -1,248 +0,0 @@
-# frozen_string_literal: true
-
-describe QA::Factory::Base do
- include Support::StubENV
-
- let(:factory) { spy('factory') }
- let(:product) { spy('product') }
- let(:product_location) { 'http://product_location' }
-
- shared_context 'fabrication context' do
- subject do
- Class.new(described_class) do
- def self.name
- 'MyFactory'
- end
- end
- end
-
- before do
- allow(subject).to receive(:current_url).and_return(product_location)
- allow(subject).to receive(:new).and_return(factory)
- allow(QA::Factory::Product).to receive(:new).with(factory).and_return(product)
- end
- end
-
- shared_examples 'fabrication method' do |fabrication_method_called, actual_fabrication_method = nil|
- let(:fabrication_method_used) { actual_fabrication_method || fabrication_method_called }
-
- it 'yields factory before calling factory method' do
- expect(factory).to receive(:something!).ordered
- expect(factory).to receive(fabrication_method_used).ordered.and_return(product_location)
-
- subject.public_send(fabrication_method_called, factory: factory) do |factory|
- factory.something!
- end
- end
-
- it 'does not log the factory and build method when QA_DEBUG=false' do
- stub_env('QA_DEBUG', 'false')
- expect(factory).to receive(fabrication_method_used).and_return(product_location)
-
- expect { subject.public_send(fabrication_method_called, 'something', factory: factory) }
- .not_to output.to_stdout
- end
- end
-
- describe '.fabricate!' do
- context 'when factory does not support fabrication via the API' do
- before do
- expect(described_class).to receive(:fabricate_via_api!).and_raise(NotImplementedError)
- end
-
- it 'calls .fabricate_via_browser_ui!' do
- expect(described_class).to receive(:fabricate_via_browser_ui!)
-
- described_class.fabricate!
- end
- end
-
- context 'when factory supports fabrication via the API' do
- it 'calls .fabricate_via_browser_ui!' do
- expect(described_class).to receive(:fabricate_via_api!)
-
- described_class.fabricate!
- end
- end
- end
-
- describe '.fabricate_via_api!' do
- include_context 'fabrication context'
-
- it_behaves_like 'fabrication method', :fabricate_via_api!
-
- it 'instantiates the factory, calls factory method returns fabrication product' do
- expect(factory).to receive(:fabricate_via_api!).and_return(product_location)
-
- result = subject.fabricate_via_api!(factory: factory, parents: [])
-
- expect(result).to eq(product)
- end
-
- it 'logs the factory and build method when QA_DEBUG=true' do
- stub_env('QA_DEBUG', 'true')
- expect(factory).to receive(:fabricate_via_api!).and_return(product_location)
-
- expect { subject.fabricate_via_api!(factory: factory, parents: []) }
- .to output(/==> Built a MyFactory via api with args \[\] in [\d\w\.\-]+/)
- .to_stdout
- end
- end
-
- describe '.fabricate_via_browser_ui!' do
- include_context 'fabrication context'
-
- it_behaves_like 'fabrication method', :fabricate_via_browser_ui!, :fabricate!
-
- it 'instantiates the factory and calls factory method' do
- subject.fabricate_via_browser_ui!('something', factory: factory, parents: [])
-
- expect(factory).to have_received(:fabricate!).with('something')
- end
-
- it 'returns fabrication product' do
- result = subject.fabricate_via_browser_ui!('something', factory: factory, parents: [])
-
- expect(result).to eq(product)
- end
-
- it 'logs the factory and build method when QA_DEBUG=true' do
- stub_env('QA_DEBUG', 'true')
-
- expect { subject.fabricate_via_browser_ui!('something', factory: factory, parents: []) }
- .to output(/==> Built a MyFactory via browser_ui with args \["something"\] in [\d\w\.\-]+/)
- .to_stdout
- end
- end
-
- shared_context 'simple factory' do
- subject do
- Class.new(QA::Factory::Base) do
- attribute :test do
- 'block'
- end
-
- attribute :no_block
-
- def fabricate!
- 'any'
- end
-
- def self.current_url
- 'http://stub'
- end
- end
- end
-
- let(:factory) { subject.new }
- end
-
- describe '.attribute' do
- include_context 'simple factory'
-
- it 'appends new product attribute' do
- expect(subject.attributes_names).to eq([:no_block, :test, :web_url])
- end
-
- context 'when the product attribute is populated via a block' do
- it 'returns a fabrication product and defines factory attributes as its methods' do
- result = subject.fabricate!(factory: factory)
-
- expect(result).to be_a(QA::Factory::Product)
- expect(result.test).to eq('block')
- end
- end
-
- context 'when the product attribute is populated via the api' do
- let(:api_resource) { { no_block: 'api' } }
-
- before do
- expect(factory).to receive(:api_resource).and_return(api_resource)
- end
-
- it 'returns a fabrication product and defines factory attributes as its methods' do
- result = subject.fabricate!(factory: factory)
-
- expect(result).to be_a(QA::Factory::Product)
- expect(result.no_block).to eq('api')
- end
-
- context 'when the attribute also has a block in the factory' do
- let(:api_resource) { { test: 'api_with_block' } }
-
- before do
- allow(QA::Runtime::Logger).to receive(:info)
- end
-
- it 'returns the api value and emits an INFO log entry' do
- result = subject.fabricate!(factory: factory)
-
- expect(result).to be_a(QA::Factory::Product)
- expect(result.test).to eq('api_with_block')
- expect(QA::Runtime::Logger)
- .to have_received(:info).with(/api_with_block/)
- end
- end
- end
-
- context 'when the product attribute is populated via a factory attribute' do
- before do
- factory.test = 'value'
- end
-
- it 'returns a fabrication product and defines factory attributes as its methods' do
- result = subject.fabricate!(factory: factory)
-
- expect(result).to be_a(QA::Factory::Product)
- expect(result.test).to eq('value')
- end
-
- context 'when the api also has such response' do
- before do
- allow(factory).to receive(:api_resource).and_return({ test: 'api' })
- end
-
- it 'returns the factory attribute for the product' do
- result = subject.fabricate!(factory: factory)
-
- expect(result).to be_a(QA::Factory::Product)
- expect(result.test).to eq('value')
- end
- end
- end
-
- context 'when the product attribute has no value' do
- it 'raises an error because no values could be found' do
- result = subject.fabricate!(factory: factory)
-
- expect { result.no_block }
- .to raise_error(described_class::NoValueError, "No value was computed for product no_block of factory #{factory.class.name}.")
- end
- end
- end
-
- describe '#web_url' do
- include_context 'simple factory'
-
- it 'sets #web_url to #current_url after fabrication' do
- subject.fabricate!(factory: factory)
-
- expect(factory.web_url).to eq(subject.current_url)
- end
- end
-
- describe '#visit!' do
- include_context 'simple factory'
-
- before do
- allow(factory).to receive(:visit)
- end
-
- it 'calls #visit with the underlying #web_url' do
- factory.web_url = subject.current_url
- factory.visit!
-
- expect(factory).to have_received(:visit).with(subject.current_url)
- end
- end
-end
diff --git a/qa/spec/factory/product_spec.rb b/qa/spec/factory/product_spec.rb
deleted file mode 100644
index 5b6eaa13e9c..00000000000
--- a/qa/spec/factory/product_spec.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-describe QA::Factory::Product do
- let(:factory) do
- Class.new(QA::Factory::Base) do
- attribute :test do
- 'block'
- end
-
- attribute :no_block
- end.new
- end
-
- let(:product) { spy('product') }
- let(:product_location) { 'http://product_location' }
-
- subject { described_class.new(factory) }
-
- before do
- factory.web_url = product_location
- end
-
- describe '.visit!' do
- it 'makes it possible to visit fabrication product' do
- allow_any_instance_of(described_class)
- .to receive(:visit).and_return('visited some url')
-
- expect(subject.visit!).to eq 'visited some url'
- end
- end
-end
diff --git a/qa/spec/factory/resource/user_spec.rb b/qa/spec/factory/resource/user_spec.rb
new file mode 100644
index 00000000000..820c506b715
--- /dev/null
+++ b/qa/spec/factory/resource/user_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+describe QA::Resource::User do
+ describe "#fabricate_via_api!" do
+ Response = Struct.new(:code, :body)
+
+ it 'fetches an existing user' do
+ existing_users = [
+ {
+ id: '0',
+ name: 'name',
+ username: 'name',
+ web_url: ''
+ }
+ ]
+ users_response = Response.new('200', JSON.dump(existing_users))
+ single_user_response = Response.new('200', JSON.dump(existing_users.first))
+
+ expect(subject).to receive(:api_get_from).with("/users?username=name").and_return(users_response)
+ expect(subject).to receive(:api_get_from).with("/users/0").and_return(single_user_response)
+
+ subject.username = 'name'
+ subject.fabricate_via_api!
+
+ expect(subject.api_response).to eq(existing_users.first)
+ end
+
+ it 'tries to create a user if it does not exist' do
+ expect(subject).to receive(:api_get_from).with("/users?username=foo").and_return(Response.new('200', '[]'))
+ expect(subject).to receive(:api_post).and_return({ web_url: '' })
+
+ subject.username = 'foo'
+ subject.fabricate_via_api!
+ end
+ end
+end
diff --git a/qa/spec/git/repository_spec.rb b/qa/spec/git/repository_spec.rb
index c629f802aa4..faa154c78da 100644
--- a/qa/spec/git/repository_spec.rb
+++ b/qa/spec/git/repository_spec.rb
@@ -26,6 +26,36 @@ describe QA::Git::Repository do
end
end
+ describe '#git_protocol=' do
+ [0, 1, 2].each do |version|
+ it "configures git to use protocol version #{version}" do
+ expect(repository).to receive(:run).with("git config protocol.version #{version}")
+ repository.git_protocol = version
+ end
+ end
+
+ it 'raises an error if the version is unsupported' do
+ expect { repository.git_protocol = 'foo' }.to raise_error(ArgumentError, "Please specify the protocol you would like to use: 0, 1, or 2")
+ end
+ end
+
+ describe '#fetch_supported_git_protocol' do
+ it "reports the detected version" do
+ expect(repository).to receive(:run).and_return("packet: git< version 2")
+ expect(repository.fetch_supported_git_protocol).to eq('2')
+ end
+
+ it 'reports unknown if version is unknown' do
+ expect(repository).to receive(:run).and_return("packet: git< version -1")
+ expect(repository.fetch_supported_git_protocol).to eq('unknown')
+ end
+
+ it 'reports unknown if content does not identify a version' do
+ expect(repository).to receive(:run).and_return("foo")
+ expect(repository.fetch_supported_git_protocol).to eq('unknown')
+ end
+ end
+
def cd_empty_temp_directory
tmp_dir = 'tmp/git-repository-spec/'
FileUtils.rm_rf(tmp_dir) if ::File.exist?(tmp_dir)
diff --git a/qa/spec/factory/api_fabricator_spec.rb b/qa/spec/resource/api_fabricator_spec.rb
index e5fbc064911..a5ed4422f6e 100644
--- a/qa/spec/factory/api_fabricator_spec.rb
+++ b/qa/spec/resource/api_fabricator_spec.rb
@@ -1,18 +1,18 @@
# frozen_string_literal: true
-describe QA::Factory::ApiFabricator do
- let(:factory_without_api_support) do
+describe QA::Resource::ApiFabricator do
+ let(:resource_without_api_support) do
Class.new do
def self.name
- 'FooBarFactory'
+ 'FooBarResource'
end
end
end
- let(:factory_with_api_support) do
+ let(:resource_with_api_support) do
Class.new do
def self.name
- 'FooBarFactory'
+ 'FooBarResource'
end
def api_get_path
@@ -33,22 +33,22 @@ describe QA::Factory::ApiFabricator do
allow(subject).to receive(:current_url).and_return('')
end
- subject { factory.tap { |f| f.include(described_class) }.new }
+ subject { resource.tap { |f| f.include(described_class) }.new }
describe '#api_support?' do
let(:api_client) { spy('Runtime::API::Client') }
let(:api_client_instance) { double('API Client') }
- context 'when factory does not support fabrication via the API' do
- let(:factory) { factory_without_api_support }
+ context 'when resource does not support fabrication via the API' do
+ let(:resource) { resource_without_api_support }
it 'returns false' do
expect(subject).not_to be_api_support
end
end
- context 'when factory supports fabrication via the API' do
- let(:factory) { factory_with_api_support }
+ context 'when resource supports fabrication via the API' do
+ let(:resource) { resource_with_api_support }
it 'returns false' do
expect(subject).to be_api_support
@@ -67,20 +67,20 @@ describe QA::Factory::ApiFabricator do
allow(api_client_instance).to receive(:personal_access_token).and_return('foo')
end
- context 'when factory does not support fabrication via the API' do
- let(:factory) { factory_without_api_support }
+ context 'when resource does not support fabrication via the API' do
+ let(:resource) { resource_without_api_support }
it 'raises a NotImplementedError exception' do
- expect { subject.fabricate_via_api! }.to raise_error(NotImplementedError, "Factory FooBarFactory does not support fabrication via the API!")
+ expect { subject.fabricate_via_api! }.to raise_error(NotImplementedError, "Resource FooBarResource does not support fabrication via the API!")
end
end
- context 'when factory supports fabrication via the API' do
- let(:factory) { factory_with_api_support }
+ context 'when resource supports fabrication via the API' do
+ let(:resource) { resource_with_api_support }
let(:api_request) { spy('Runtime::API::Request') }
let(:resource_web_url) { 'http://example.org/api/v4/foo' }
- let(:resource) { { id: 1, name: 'John Doe', web_url: resource_web_url } }
- let(:raw_post) { double('Raw POST response', code: 201, body: resource.to_json) }
+ let(:response) { { id: 1, name: 'John Doe', web_url: resource_web_url } }
+ let(:raw_post) { double('Raw POST response', code: 201, body: response.to_json) }
before do
stub_const('QA::Runtime::API::Request', api_request)
@@ -103,7 +103,7 @@ describe QA::Factory::ApiFabricator do
it 'populates api_resource with the resource' do
subject.fabricate_via_api!
- expect(subject.api_resource).to eq(resource)
+ expect(subject.api_resource).to eq(response)
end
context 'when the POST fails' do
@@ -114,17 +114,17 @@ describe QA::Factory::ApiFabricator do
expect(api_request).to receive(:new).with(api_client_instance, subject.api_post_path).and_return(double(url: resource_web_url))
expect(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(raw_post)
- expect { subject.fabricate_via_api! }.to raise_error(described_class::ResourceFabricationFailedError, "Fabrication of FooBarFactory using the API failed (400) with `#{raw_post}`.")
+ expect { subject.fabricate_via_api! }.to raise_error(described_class::ResourceFabricationFailedError, "Fabrication of FooBarResource using the API failed (400) with `#{raw_post}`.")
expect(subject.api_resource).to be_nil
end
end
end
context '#transform_api_resource' do
- let(:factory) do
+ let(:resource) do
Class.new do
def self.name
- 'FooBarFactory'
+ 'FooBarResource'
end
def api_get_path
@@ -146,12 +146,12 @@ describe QA::Factory::ApiFabricator do
end
end
- let(:resource) { { existing: 'foo', web_url: resource_web_url } }
+ let(:response) { { existing: 'foo', web_url: resource_web_url } }
let(:transformed_resource) { { existing: 'foo', new: 'foobar', web_url: resource_web_url } }
it 'transforms the resource' do
expect(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(raw_post)
- expect(subject).to receive(:transform_api_resource).with(resource).and_return(transformed_resource)
+ expect(subject).to receive(:transform_api_resource).with(response).and_return(transformed_resource)
subject.fabricate_via_api!
end
diff --git a/qa/spec/resource/base_spec.rb b/qa/spec/resource/base_spec.rb
new file mode 100644
index 00000000000..dc9e16792d3
--- /dev/null
+++ b/qa/spec/resource/base_spec.rb
@@ -0,0 +1,246 @@
+# frozen_string_literal: true
+
+describe QA::Resource::Base do
+ include Support::StubENV
+
+ let(:resource) { spy('resource') }
+ let(:location) { 'http://location' }
+
+ shared_context 'fabrication context' do
+ subject do
+ Class.new(described_class) do
+ def self.name
+ 'MyResource'
+ end
+ end
+ end
+
+ before do
+ allow(subject).to receive(:current_url).and_return(location)
+ allow(subject).to receive(:new).and_return(resource)
+ end
+ end
+
+ shared_examples 'fabrication method' do |fabrication_method_called, actual_fabrication_method = nil|
+ let(:fabrication_method_used) { actual_fabrication_method || fabrication_method_called }
+
+ it 'yields resource before calling resource method' do
+ expect(resource).to receive(:something!).ordered
+ expect(resource).to receive(fabrication_method_used).ordered.and_return(location)
+
+ subject.public_send(fabrication_method_called, resource: resource) do |resource|
+ resource.something!
+ end
+ end
+
+ it 'does not log the resource and build method when QA_DEBUG=false' do
+ stub_env('QA_DEBUG', 'false')
+ expect(resource).to receive(fabrication_method_used).and_return(location)
+
+ expect { subject.public_send(fabrication_method_called, 'something', resource: resource) }
+ .not_to output.to_stdout
+ end
+ end
+
+ describe '.fabricate!' do
+ context 'when resource does not support fabrication via the API' do
+ before do
+ expect(described_class).to receive(:fabricate_via_api!).and_raise(NotImplementedError)
+ end
+
+ it 'calls .fabricate_via_browser_ui!' do
+ expect(described_class).to receive(:fabricate_via_browser_ui!)
+
+ described_class.fabricate!
+ end
+ end
+
+ context 'when resource supports fabrication via the API' do
+ it 'calls .fabricate_via_browser_ui!' do
+ expect(described_class).to receive(:fabricate_via_api!)
+
+ described_class.fabricate!
+ end
+ end
+ end
+
+ describe '.fabricate_via_api!' do
+ include_context 'fabrication context'
+
+ it_behaves_like 'fabrication method', :fabricate_via_api!
+
+ it 'instantiates the resource, calls resource method returns the resource' do
+ expect(resource).to receive(:fabricate_via_api!).and_return(location)
+
+ result = subject.fabricate_via_api!(resource: resource, parents: [])
+
+ expect(result).to eq(resource)
+ end
+
+ it 'logs the resource and build method when QA_DEBUG=true' do
+ stub_env('QA_DEBUG', 'true')
+ expect(resource).to receive(:fabricate_via_api!).and_return(location)
+
+ expect { subject.fabricate_via_api!('something', resource: resource, parents: []) }
+ .to output(/==> Built a MyResource via api in [\d\.\-e]+ seconds+/)
+ .to_stdout
+ end
+ end
+
+ describe '.fabricate_via_browser_ui!' do
+ include_context 'fabrication context'
+
+ it_behaves_like 'fabrication method', :fabricate_via_browser_ui!, :fabricate!
+
+ it 'instantiates the resource and calls resource method' do
+ subject.fabricate_via_browser_ui!('something', resource: resource, parents: [])
+
+ expect(resource).to have_received(:fabricate!).with('something')
+ end
+
+ it 'returns fabrication resource' do
+ result = subject.fabricate_via_browser_ui!('something', resource: resource, parents: [])
+
+ expect(result).to eq(resource)
+ end
+
+ it 'logs the resource and build method when QA_DEBUG=true' do
+ stub_env('QA_DEBUG', 'true')
+
+ expect { subject.fabricate_via_browser_ui!('something', resource: resource, parents: []) }
+ .to output(/==> Built a MyResource via browser_ui in [\d\.\-e]+ seconds+/)
+ .to_stdout
+ end
+ end
+
+ shared_context 'simple resource' do
+ subject do
+ Class.new(QA::Resource::Base) do
+ attribute :test do
+ 'block'
+ end
+
+ attribute :no_block
+
+ def fabricate!
+ 'any'
+ end
+
+ def self.current_url
+ 'http://stub'
+ end
+ end
+ end
+
+ let(:resource) { subject.new }
+ end
+
+ describe '.attribute' do
+ include_context 'simple resource'
+
+ it 'appends new attribute' do
+ expect(subject.attributes_names).to eq([:no_block, :test, :web_url])
+ end
+
+ context 'when the attribute is populated via a block' do
+ it 'returns value from the block' do
+ result = subject.fabricate!(resource: resource)
+
+ expect(result).to be_a(described_class)
+ expect(result.test).to eq('block')
+ end
+ end
+
+ context 'when the attribute is populated via the api' do
+ let(:api_resource) { { no_block: 'api' } }
+
+ before do
+ expect(resource).to receive(:api_resource).and_return(api_resource)
+ end
+
+ it 'returns value from api' do
+ result = subject.fabricate!(resource: resource)
+
+ expect(result).to be_a(described_class)
+ expect(result.no_block).to eq('api')
+ end
+
+ context 'when the attribute also has a block' do
+ let(:api_resource) { { test: 'api_with_block' } }
+
+ before do
+ allow(QA::Runtime::Logger).to receive(:info)
+ end
+
+ it 'returns value from api and emits an INFO log entry' do
+ result = subject.fabricate!(resource: resource)
+
+ expect(result).to be_a(described_class)
+ expect(result.test).to eq('api_with_block')
+ expect(QA::Runtime::Logger)
+ .to have_received(:info).with(/api_with_block/)
+ end
+ end
+ end
+
+ context 'when the attribute is populated via direct assignment' do
+ before do
+ resource.test = 'value'
+ end
+
+ it 'returns value from the assignment' do
+ result = subject.fabricate!(resource: resource)
+
+ expect(result).to be_a(described_class)
+ expect(result.test).to eq('value')
+ end
+
+ context 'when the api also has such response' do
+ before do
+ allow(resource).to receive(:api_resource).and_return({ test: 'api' })
+ end
+
+ it 'returns value from the assignment' do
+ result = subject.fabricate!(resource: resource)
+
+ expect(result).to be_a(described_class)
+ expect(result.test).to eq('value')
+ end
+ end
+ end
+
+ context 'when the attribute has no value' do
+ it 'raises an error because no values could be found' do
+ result = subject.fabricate!(resource: resource)
+
+ expect { result.no_block }
+ .to raise_error(described_class::NoValueError, "No value was computed for no_block of #{resource.class.name}.")
+ end
+ end
+ end
+
+ describe '#web_url' do
+ include_context 'simple resource'
+
+ it 'sets #web_url to #current_url after fabrication' do
+ subject.fabricate!(resource: resource)
+
+ expect(resource.web_url).to eq(subject.current_url)
+ end
+ end
+
+ describe '#visit!' do
+ include_context 'simple resource'
+
+ before do
+ allow(resource).to receive(:visit)
+ end
+
+ it 'calls #visit with the underlying #web_url' do
+ resource.web_url = subject.current_url
+ resource.visit!
+
+ expect(resource).to have_received(:visit).with(subject.current_url)
+ end
+ end
+end
diff --git a/qa/spec/factory/repository/push_spec.rb b/qa/spec/resource/repository/push_spec.rb
index 2eb6c008248..bf3ebce0cfe 100644
--- a/qa/spec/factory/repository/push_spec.rb
+++ b/qa/spec/resource/repository/push_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-describe QA::Factory::Repository::Push do
+describe QA::Resource::Repository::Push do
describe '.files=' do
let(:files) do
[
diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb
index c59c415c148..ded51d5bb7c 100644
--- a/qa/spec/runtime/env_spec.rb
+++ b/qa/spec/runtime/env_spec.rb
@@ -3,49 +3,62 @@
describe QA::Runtime::Env do
include Support::StubENV
- shared_examples 'boolean method' do |method, env_key, default|
+ shared_examples 'boolean method' do |**kwargs|
+ it_behaves_like 'boolean method with parameter', kwargs
+ end
+
+ shared_examples 'boolean method with parameter' do |method:, param: nil, env_key:, default:|
context 'when there is an env variable set' do
it 'returns false when falsey values specified' do
stub_env(env_key, 'false')
- expect(described_class.public_send(method)).to be_falsey
+ expect(described_class.public_send(method, *param)).to be_falsey
stub_env(env_key, 'no')
- expect(described_class.public_send(method)).to be_falsey
+ expect(described_class.public_send(method, *param)).to be_falsey
stub_env(env_key, '0')
- expect(described_class.public_send(method)).to be_falsey
+ expect(described_class.public_send(method, *param)).to be_falsey
end
it 'returns true when anything else specified' do
stub_env(env_key, 'true')
- expect(described_class.public_send(method)).to be_truthy
+ expect(described_class.public_send(method, *param)).to be_truthy
stub_env(env_key, '1')
- expect(described_class.public_send(method)).to be_truthy
+ expect(described_class.public_send(method, *param)).to be_truthy
stub_env(env_key, 'anything')
- expect(described_class.public_send(method)).to be_truthy
+ expect(described_class.public_send(method, *param)).to be_truthy
end
end
context 'when there is no env variable set' do
it "returns the default, #{default}" do
stub_env(env_key, nil)
- expect(described_class.public_send(method)).to be(default)
+ expect(described_class.public_send(method, *param)).to be(default)
end
end
end
describe '.signup_disabled?' do
- it_behaves_like 'boolean method', :signup_disabled?, 'SIGNUP_DISABLED', false
+ it_behaves_like 'boolean method',
+ method: :signup_disabled?,
+ env_key: 'SIGNUP_DISABLED',
+ default: false
end
describe '.debug?' do
- it_behaves_like 'boolean method', :debug?, 'QA_DEBUG', false
+ it_behaves_like 'boolean method',
+ method: :debug?,
+ env_key: 'QA_DEBUG',
+ default: false
end
describe '.chrome_headless?' do
- it_behaves_like 'boolean method', :chrome_headless?, 'CHROME_HEADLESS', true
+ it_behaves_like 'boolean method',
+ method: :chrome_headless?,
+ env_key: 'CHROME_HEADLESS',
+ default: true
end
describe '.running_in_ci?' do
@@ -182,4 +195,16 @@ describe QA::Runtime::Env do
expect(described_class.log_destination).to eq('path/to_file')
end
end
+
+ describe '.can_test?' do
+ it_behaves_like 'boolean method with parameter',
+ method: :can_test?,
+ param: :git_protocol_v2,
+ env_key: 'QA_CAN_TEST_GIT_PROTOCOL_V2',
+ default: true
+
+ it 'raises ArgumentError if feature is unknown' do
+ expect { described_class.can_test? :foo }.to raise_error(ArgumentError, 'Unknown feature "foo"')
+ end
+ end
end
diff --git a/qa/spec/specs/runner_spec.rb b/qa/spec/specs/runner_spec.rb
index 9ddaf7ab1b3..741821ddf8c 100644
--- a/qa/spec/specs/runner_spec.rb
+++ b/qa/spec/specs/runner_spec.rb
@@ -76,6 +76,20 @@ describe QA::Specs::Runner do
end
end
+ context 'when git protocol v2 is not supported' do
+ before do
+ allow(QA::Runtime::Env).to receive(:can_test?).with(:git_protocol_v2).and_return(false)
+ end
+
+ subject { described_class.new }
+
+ it 'it includes default args and excludes the requires_git_protocol_v2 tag' do
+ expect_rspec_runner_arguments(['--tag', '~orchestrated', '--tag', '~requires_git_protocol_v2', *described_class::DEFAULT_TEST_PATH_ARGS])
+
+ subject.perform
+ end
+ end
+
def expect_rspec_runner_arguments(arguments)
expect(RSpec::Core::Runner).to receive(:run)
.with(arguments, $stderr, $stdout)
diff --git a/rubocop/cop/code_reuse/active_record.rb b/rubocop/cop/code_reuse/active_record.rb
index d25e8548fd0..2be8f7c11aa 100644
--- a/rubocop/cop/code_reuse/active_record.rb
+++ b/rubocop/cop/code_reuse/active_record.rb
@@ -49,7 +49,6 @@ module RuboCop
limit: true,
lock: false,
many?: false,
- none: false,
offset: true,
order: true,
pluck: true,
diff --git a/scripts/build_assets_image b/scripts/build_assets_image
new file mode 100755
index 00000000000..1d77524d503
--- /dev/null
+++ b/scripts/build_assets_image
@@ -0,0 +1,21 @@
+#!/bin/bash
+
+# Generate the image name based on the project this is being run in
+ASSETS_IMAGE_NAME=$(echo ${CI_PROJECT_NAME} |
+ awk '{
+ split($1, p, "-");
+ interim = sprintf("%s-assets-%s", p[1], p[2]);
+ sub(/-$/, "", interim);
+ print interim
+ }'
+)
+
+ASSETS_IMAGE_PATH=${CI_REGISTRY}/${CI_PROJECT_PATH}/${ASSETS_IMAGE_NAME}
+
+mkdir -p assets_container.build/public
+cp -r public/assets assets_container.build/public/
+cp Dockerfile.assets assets_container.build/
+docker build -t ${ASSETS_IMAGE_PATH}:${CI_COMMIT_REF_SLUG} -f assets_container.build/Dockerfile.assets assets_container.build/
+docker login -u gitlab-ci-token -p ${CI_JOB_TOKEN} ${CI_REGISTRY}
+docker push ${ASSETS_IMAGE_PATH}
+
diff --git a/scripts/rails4-gemfile-lock-check b/scripts/rails4-gemfile-lock-check
new file mode 100755
index 00000000000..a74a49874e1
--- /dev/null
+++ b/scripts/rails4-gemfile-lock-check
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+
+echo -e "=> Checking if Gemfile.rails4.lock is up-to-date...\\n"
+
+cp Gemfile.rails4.lock Gemfile.rails4.lock.orig
+BUNDLE_GEMFILE=Gemfile.rails4 bundle install "$BUNDLE_INSTALL_FLAGS"
+diff -u Gemfile.rails4.lock.orig Gemfile.rails4.lock >/dev/null 2>&1
+
+if [ $? == 1 ]
+then
+ diff -u Gemfile.rails4.lock.orig Gemfile.rails4.lock
+
+ echo -e "\\n✖ ERROR: Gemfile.rails4.lock is not up-to-date!
+ Please run 'BUNDLE_GEMFILE=Gemfile.rails4 bundle install'\\n" >&2
+ exit 1
+fi
+
+echo "✔ Gemfile.rails4.lock is up-to-date"
+exit 0
diff --git a/scripts/rails5-gemfile-lock-check b/scripts/rails5-gemfile-lock-check
deleted file mode 100755
index da6f1b7145e..00000000000
--- a/scripts/rails5-gemfile-lock-check
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/usr/bin/env bash
-
-echo -e "=> Checking if Gemfile.rails5.lock is up-to-date...\\n"
-
-cp Gemfile.rails5.lock Gemfile.rails5.lock.orig
-BUNDLE_GEMFILE=Gemfile.rails5 bundle install "$BUNDLE_INSTALL_FLAGS"
-diff -u Gemfile.rails5.lock.orig Gemfile.rails5.lock >/dev/null 2>&1
-
-if [ $? == 1 ]
-then
- diff -u Gemfile.rails5.lock.orig Gemfile.rails5.lock
-
- echo -e "\\n✖ ERROR: Gemfile.rails5.lock is not up-to-date!
- Please run 'BUNDLE_GEMFILE=Gemfile.rails5 bundle install'\\n" >&2
- exit 1
-fi
-
-echo "✔ Gemfile.rails5.lock is up-to-date"
-exit 0
diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh
index d372bcbdab1..00e23f12bc0 100755
--- a/scripts/review_apps/review-apps.sh
+++ b/scripts/review_apps/review-apps.sh
@@ -126,6 +126,9 @@ function deploy() {
delete
cleanup
fi
+
+ create_secret
+
helm repo add gitlab https://charts.gitlab.io/
helm dep update .
@@ -133,6 +136,7 @@ HELM_CMD=$(cat << EOF
helm upgrade --install \
--wait \
--timeout 600 \
+ --set global.appConfig.enableUsagePing=false \
--set releaseOverride="$CI_ENVIRONMENT_SLUG" \
--set global.hosts.hostSuffix="$HOST_SUFFIX" \
--set global.hosts.domain="$REVIEW_APPS_DOMAIN" \
@@ -175,21 +179,35 @@ function delete() {
track="${1-stable}"
name="$CI_ENVIRONMENT_SLUG"
+ if [ -z "$CI_ENVIRONMENT_SLUG" ]; then
+ echo "No release given, aborting the delete!"
+ return
+ fi
+
if [[ "$track" != "stable" ]]; then
name="$name-$track"
fi
+ if ! deployExists "${KUBE_NAMESPACE}" "${name}"; then
+ echo "The release $name doesn't exist, aborting the cleanup!"
+ return
+ fi
+
echo "Deleting release '$name'..."
helm delete --purge "$name" || true
}
function cleanup() {
- echo "Cleaning up $CI_ENVIRONMENT_SLUG..."
- kubectl -n "$KUBE_NAMESPACE" get ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa 2>&1 \
- | grep "$CI_ENVIRONMENT_SLUG" \
- | awk '{print $1}' \
- | xargs kubectl -n "$KUBE_NAMESPACE" delete \
- || true
+ if [ -z "$CI_ENVIRONMENT_SLUG" ]; then
+ echo "No release given, aborting the delete!"
+ return
+ fi
+
+ echo "Cleaning up '$CI_ENVIRONMENT_SLUG'..."
+ 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
}
function install_external_dns() {
diff --git a/scripts/static-analysis b/scripts/static-analysis
index 0e67eabfec1..25ba7ec6c8e 100755
--- a/scripts/static-analysis
+++ b/scripts/static-analysis
@@ -29,6 +29,7 @@ tasks = [
%w[bin/rake lint:all],
%w[bundle exec license_finder],
%w[yarn run eslint],
+ %w[yarn run prettier-all],
%w[bundle exec rubocop --parallel],
%w[scripts/lint-conflicts.sh],
%w[scripts/lint-rugged]
diff --git a/scripts/trigger-build b/scripts/trigger-build
index b76cd5dd6f0..873c41db456 100755
--- a/scripts/trigger-build
+++ b/scripts/trigger-build
@@ -32,32 +32,32 @@ module Trigger
private
- # Must be overriden
+ # Must be overridden
def downstream_project_path
raise NotImplementedError
end
- # Must be overriden
+ # Must be overridden
def ref
raise NotImplementedError
end
- # Must be overriden
+ # Must be overridden
def trigger_token
raise NotImplementedError
end
- # Must be overriden
+ # Must be overridden
def access_token
raise NotImplementedError
end
- # Can be overriden
+ # Can be overridden
def extra_variables
{}
end
- # Can be overriden
+ # Can be overridden
def version_param_value(version_file)
File.read(version_file).strip
end
@@ -107,7 +107,8 @@ module Trigger
{
'GITLAB_VERSION' => ENV['CI_COMMIT_SHA'],
'ALTERNATIVE_SOURCES' => 'true',
- 'ee' => Trigger.ee? ? 'true' : 'false'
+ 'ee' => Trigger.ee? ? 'true' : 'false',
+ 'QA_BRANCH' => ENV['QA_BRANCH'] || 'master'
}
end
end
diff --git a/spec/controllers/concerns/send_file_upload_spec.rb b/spec/controllers/concerns/send_file_upload_spec.rb
index 767fba7fd58..379b2d6b935 100644
--- a/spec/controllers/concerns/send_file_upload_spec.rb
+++ b/spec/controllers/concerns/send_file_upload_spec.rb
@@ -28,8 +28,9 @@ describe SendFileUpload do
describe '#send_upload' do
let(:controller) { controller_class.new }
let(:temp_file) { Tempfile.new('test') }
+ let(:params) { {} }
- subject { controller.send_upload(uploader) }
+ subject { controller.send_upload(uploader, **params) }
before do
FileUtils.touch(temp_file)
@@ -52,7 +53,7 @@ describe SendFileUpload do
end
context 'with attachment' do
- let(:send_attachment) { controller.send_upload(uploader, attachment: 'test.js') }
+ let(:params) { { attachment: 'test.js' } }
it 'sends a file with content-type of text/plain' do
expected_params = {
@@ -62,7 +63,7 @@ describe SendFileUpload do
}
expect(controller).to receive(:send_file).with(uploader.path, expected_params)
- send_attachment
+ subject
end
context 'with a proxied file in object storage' do
@@ -75,7 +76,7 @@ describe SendFileUpload do
it 'sends a file with a custom type' do
headers = double
- expected_headers = %r(response-content-disposition=attachment%3Bfilename%3D%22test.js%22&response-content-type=application/javascript)
+ expected_headers = %r(response-content-disposition=attachment%3Bfilename%3D%22test.js%22&response-content-type=application/ecmascript)
expect(Gitlab::Workhorse).to receive(:send_url).with(expected_headers).and_call_original
expect(headers).to receive(:store).with(Gitlab::Workhorse::SEND_DATA_HEADER, /^send-url:/)
@@ -83,7 +84,7 @@ describe SendFileUpload do
expect(controller).to receive(:headers) { headers }
expect(controller).to receive(:head).with(:ok)
- send_attachment
+ subject
end
end
end
@@ -95,11 +96,7 @@ describe SendFileUpload do
uploader.store!(temp_file)
end
- context 'and proxying is enabled' do
- before do
- allow(Gitlab.config.uploads.object_store).to receive(:proxy_download) { true }
- end
-
+ shared_examples 'proxied file' do
it 'sends a file' do
headers = double
expect(Gitlab::Workhorse).not_to receive(:send_url).with(/response-content-disposition/)
@@ -115,6 +112,14 @@ describe SendFileUpload do
end
end
+ context 'and proxying is enabled' do
+ before do
+ allow(Gitlab.config.uploads.object_store).to receive(:proxy_download) { true }
+ end
+
+ it_behaves_like 'proxied file'
+ end
+
context 'and proxying is disabled' do
before do
allow(Gitlab.config.uploads.object_store).to receive(:proxy_download) { false }
@@ -125,6 +130,12 @@ describe SendFileUpload do
subject
end
+
+ context 'with proxy requested' do
+ let(:params) { { proxy: true } }
+
+ it_behaves_like 'proxied file'
+ end
end
end
end
diff --git a/spec/controllers/groups/boards_controller_spec.rb b/spec/controllers/groups/boards_controller_spec.rb
index f7a4a4192d6..99429c93b82 100644
--- a/spec/controllers/groups/boards_controller_spec.rb
+++ b/spec/controllers/groups/boards_controller_spec.rb
@@ -32,12 +32,13 @@ describe Groups::BoardsController do
end
it 'renders template if visited board is not found' do
- visited = double
+ temporary_board = create(:board, group: group)
+ visited = create(:board_group_recent_visit, group: temporary_board.group, board: temporary_board, user: user)
+ temporary_board.delete
- allow(visited).to receive(:board_id).and_return(12)
allow_any_instance_of(Boards::Visits::LatestService).to receive(:execute).and_return(visited)
- list_boards format: :html
+ list_boards
expect(response).to render_template :index
expect(response.content_type).to eq 'text/html'
diff --git a/spec/controllers/groups/clusters/applications_controller_spec.rb b/spec/controllers/groups/clusters/applications_controller_spec.rb
new file mode 100644
index 00000000000..68a798542b6
--- /dev/null
+++ b/spec/controllers/groups/clusters/applications_controller_spec.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Groups::Clusters::ApplicationsController do
+ include AccessMatchersForController
+
+ def current_application
+ Clusters::Cluster::APPLICATIONS[application]
+ end
+
+ describe 'POST create' do
+ let(:cluster) { create(:cluster, :group, :provided_by_gcp) }
+ let(:group) { cluster.group }
+ let(:application) { 'helm' }
+ let(:params) { { application: application, id: cluster.id } }
+
+ describe 'functionality' do
+ let(:user) { create(:user) }
+
+ before do
+ group.add_maintainer(user)
+ sign_in(user)
+ end
+
+ it 'schedule an application installation' do
+ expect(ClusterInstallAppWorker).to receive(:perform_async).with(application, anything).once
+
+ expect { go }.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 { go }.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
+ go
+
+ expect(response).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
+ go
+
+ expect(response).to have_http_status(:bad_request)
+ end
+ end
+ end
+
+ describe 'security' do
+ before do
+ allow(ClusterInstallAppWorker).to receive(:perform_async)
+ end
+
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(group) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(group) }
+ it { expect { go }.to be_denied_for(:developer).of(group) }
+ it { expect { go }.to be_denied_for(:reporter).of(group) }
+ it { expect { go }.to be_denied_for(:guest).of(group) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+
+ def go
+ post :create, params.merge(group_id: group)
+ end
+ end
+end
diff --git a/spec/controllers/groups/clusters_controller_spec.rb b/spec/controllers/groups/clusters_controller_spec.rb
new file mode 100644
index 00000000000..6e130f830a2
--- /dev/null
+++ b/spec/controllers/groups/clusters_controller_spec.rb
@@ -0,0 +1,574 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Groups::ClustersController do
+ include AccessMatchersForController
+ include GoogleApi::CloudPlatformHelpers
+
+ set(:group) { create(:group) }
+
+ let(:user) { create(:user) }
+
+ before do
+ group.add_maintainer(user)
+ sign_in(user)
+ end
+
+ describe 'GET index' do
+ def go(params = {})
+ get :index, params.reverse_merge(group_id: group)
+ end
+
+ context 'when feature flag is not enabled' do
+ before do
+ stub_feature_flags(group_clusters: false)
+ end
+
+ it 'renders 404' do
+ go
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when feature flag is enabled' do
+ before do
+ stub_feature_flags(group_clusters: true)
+ end
+
+ describe 'functionality' do
+ context 'when group has one or more clusters' do
+ let(:group) { create(:group) }
+
+ let!(:enabled_cluster) do
+ create(:cluster, :provided_by_gcp, cluster_type: :group_type, groups: [group])
+ end
+
+ let!(:disabled_cluster) do
+ create(:cluster, :disabled, :provided_by_gcp, :production_environment, cluster_type: :group_type, groups: [group])
+ end
+
+ it 'lists available clusters' do
+ go
+
+ 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) { group.clusters.page.total_pages }
+
+ before do
+ allow(Clusters::Cluster).to receive(:paginates_per).and_return(1)
+ create_list(:cluster, 2, :provided_by_gcp, :production_environment, cluster_type: :group_type, groups: [group])
+ end
+
+ it 'redirects to the page' do
+ go(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 group does not have a cluster' do
+ let(:group) { create(:group) }
+
+ it 'returns an empty state page' do
+ go
+
+ 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, cluster_type: :group_type, groups: [group]) }
+
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(group) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(group) }
+ it { expect { go }.to be_denied_for(:developer).of(group) }
+ it { expect { go }.to be_denied_for(:reporter).of(group) }
+ it { expect { go }.to be_denied_for(:guest).of(group) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'GET new' do
+ def go
+ get :new, group_id: group
+ 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
+ go
+
+ expect(assigns(:authorize_url)).to include(key)
+ expect(session[session_key_for_redirect_uri]).to eq(new_group_cluster_path(group))
+ end
+ end
+
+ context 'when omniauth has not configured' do
+ before do
+ stub_omniauth_setting(providers: [])
+ end
+
+ it 'does not have authorize_url' do
+ go
+
+ 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
+ go
+
+ 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
+ go
+
+ expect(assigns(:user_cluster)).to be_an_instance_of(Clusters::ClusterPresenter)
+ end
+ end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(group) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(group) }
+ it { expect { go }.to be_denied_for(:developer).of(group) }
+ it { expect { go }.to be_denied_for(:reporter).of(group) }
+ it { expect { go }.to be_denied_for(:guest).of(group) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'POST create for new cluster' 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 go
+ post :create_gcp, params.merge(group_id: group)
+ 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 { go }.to change { Clusters::Cluster.count }
+ .and change { Clusters::Providers::Gcp.count }
+
+ cluster = group.clusters.first
+
+ expect(response).to redirect_to(group_cluster_path(group, 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 { go }.to change { Clusters::Cluster.count }
+ .and change { Clusters::Providers::Gcp.count }
+ expect(group.clusters.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 { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(group) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(group) }
+ it { expect { go }.to be_denied_for(:developer).of(group) }
+ it { expect { go }.to be_denied_for(:reporter).of(group) }
+ it { expect { go }.to be_denied_for(:guest).of(group) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'POST create for existing cluster' do
+ let(:params) do
+ {
+ cluster: {
+ name: 'new-cluster',
+ platform_kubernetes_attributes: {
+ api_url: 'http://my-url',
+ token: 'test'
+ }
+ }
+ }
+ end
+
+ def go
+ post :create_user, params.merge(group_id: group)
+ end
+
+ describe 'functionality' do
+ context 'when creates a cluster' do
+ it 'creates a new cluster' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+
+ expect { go }.to change { Clusters::Cluster.count }
+ .and change { Clusters::Platforms::Kubernetes.count }
+
+ cluster = group.clusters.first
+
+ expect(response).to redirect_to(group_cluster_path(group, 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 { go }.to change { Clusters::Cluster.count }
+ .and change { Clusters::Platforms::Kubernetes.count }
+
+ cluster = group.clusters.first
+
+ expect(response).to redirect_to(group_cluster_path(group, 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 { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(group) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(group) }
+ it { expect { go }.to be_denied_for(:developer).of(group) }
+ it { expect { go }.to be_denied_for(:reporter).of(group) }
+ it { expect { go }.to be_denied_for(:guest).of(group) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'GET cluster_status' do
+ let(:cluster) { create(:cluster, :providing_by_gcp, cluster_type: :group_type, groups: [group]) }
+
+ def go
+ get :cluster_status,
+ group_id: group.to_param,
+ id: cluster,
+ format: :json
+ end
+
+ describe 'functionality' do
+ it 'responds with matching schema' do
+ go
+
+ 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)
+
+ go
+ end
+ end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(group) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(group) }
+ it { expect { go }.to be_denied_for(:developer).of(group) }
+ it { expect { go }.to be_denied_for(:reporter).of(group) }
+ it { expect { go }.to be_denied_for(:guest).of(group) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'GET show' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, cluster_type: :group_type, groups: [group]) }
+
+ def go
+ get :show,
+ group_id: group,
+ id: cluster
+ end
+
+ describe 'functionality' do
+ it 'renders view' do
+ go
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(assigns(:cluster)).to eq(cluster)
+ end
+ end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(group) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(group) }
+ it { expect { go }.to be_denied_for(:developer).of(group) }
+ it { expect { go }.to be_denied_for(:reporter).of(group) }
+ it { expect { go }.to be_denied_for(:guest).of(group) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'PUT update' do
+ def go(format: :html)
+ put :update, params.merge(
+ group_id: group.to_param,
+ id: cluster,
+ format: format
+ )
+ end
+
+ let(:cluster) { create(:cluster, :provided_by_user, cluster_type: :group_type, groups: [group]) }
+
+ let(:params) do
+ {
+ cluster: {
+ enabled: false,
+ name: 'my-new-cluster-name'
+ }
+ }
+ end
+
+ it 'updates and redirects back to show page' do
+ go
+
+ cluster.reload
+ expect(response).to redirect_to(group_cluster_path(group, 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')
+ 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'
+ }
+ }
+ end
+
+ it 'updates and redirects back to show page' do
+ go(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
+ go(format: :json)
+
+ expect(response).to have_http_status(:bad_request)
+ end
+ end
+ end
+ end
+
+ describe 'security' do
+ set(:cluster) { create(:cluster, :provided_by_gcp, cluster_type: :group_type, groups: [group]) }
+
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(group) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(group) }
+ it { expect { go }.to be_denied_for(:developer).of(group) }
+ it { expect { go }.to be_denied_for(:reporter).of(group) }
+ it { expect { go }.to be_denied_for(:guest).of(group) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'DELETE destroy' do
+ let!(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, cluster_type: :group_type, groups: [group]) }
+
+ def go
+ delete :destroy,
+ group_id: group,
+ 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 { go }
+ .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(group_clusters_path(group))
+ 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, cluster_type: :group_type, groups: [group]) }
+
+ it 'destroys and redirects back to clusters list' do
+ expect { go }
+ .to change { Clusters::Cluster.count }.by(-1)
+ .and change { Clusters::Providers::Gcp.count }.by(-1)
+
+ expect(response).to redirect_to(group_clusters_path(group))
+ 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, cluster_type: :group_type, groups: [group]) }
+
+ it 'destroys and redirects back to clusters list' do
+ expect { go }
+ .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(group_clusters_path(group))
+ 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, cluster_type: :group_type, groups: [group]) }
+
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(group) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(group) }
+ it { expect { go }.to be_denied_for(:developer).of(group) }
+ it { expect { go }.to be_denied_for(:reporter).of(group) }
+ it { expect { go }.to be_denied_for(:guest).of(group) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+ end
+
+ context 'no group_id param' do
+ it 'does not respond to any action without group_id param' do
+ expect { get :index }.to raise_error(ActionController::UrlGenerationError)
+ end
+ end
+end
diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb
index 465f3499703..42723bb3820 100644
--- a/spec/controllers/groups/milestones_controller_spec.rb
+++ b/spec/controllers/groups/milestones_controller_spec.rb
@@ -63,7 +63,7 @@ describe Groups::MilestonesController do
let(:group_milestone) { create(:milestone, group: group) }
context 'when there is a title parameter' do
- it 'searchs for a legacy group milestone' do
+ it 'searches for a legacy group milestone' do
expect(GlobalMilestone).to receive(:build)
expect(Milestone).not_to receive(:find_by_iid)
@@ -72,7 +72,7 @@ describe Groups::MilestonesController do
end
context 'when there is not a title parameter' do
- it 'searchs for a group milestone' do
+ it 'searches for a group milestone' do
expect(GlobalMilestone).not_to receive(:build)
expect(Milestone).to receive(:find_by_iid)
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index a099cdafa58..4de61b65f71 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -529,7 +529,7 @@ describe GroupsController do
sign_in(user)
end
- context 'when transfering to a subgroup goes right' do
+ context 'when transferring to a subgroup goes right' do
let(:new_parent_group) { create(:group, :public) }
let!(:group_member) { create(:group_member, :owner, group: group, user: user) }
let!(:new_parent_group_member) { create(:group_member, :owner, group: new_parent_group, user: user) }
diff --git a/spec/controllers/import/bitbucket_server_controller_spec.rb b/spec/controllers/import/bitbucket_server_controller_spec.rb
index 5024ef71771..77060fdc3be 100644
--- a/spec/controllers/import/bitbucket_server_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_server_controller_spec.rb
@@ -121,12 +121,19 @@ describe Import::BitbucketServerController do
@repo = double(slug: 'vim', project_key: 'asd', full_name: 'asd/vim', "valid?" => true, project_name: 'asd', browse_url: 'http://test', name: 'vim')
@invalid_repo = double(slug: 'invalid', project_key: 'foobar', full_name: 'asd/foobar', "valid?" => false, browse_url: 'http://bad-repo')
+ @created_repo = double(slug: 'created', project_key: 'existing', full_name: 'group/created', "valid?" => true, browse_url: 'http://existing')
assign_session_tokens
end
it 'assigns repository categories' do
- created_project = create(:project, import_type: 'bitbucket_server', creator_id: user.id, import_source: 'foo/bar', import_status: 'finished')
- expect(client).to receive(:repos).and_return([@repo, @invalid_repo])
+ created_project = create(:project, import_type: 'bitbucket_server', creator_id: user.id, import_status: 'finished', import_source: @created_repo.browse_url)
+ repos = instance_double(BitbucketServer::Collection)
+
+ expect(repos).to receive(:partition).and_return([[@repo, @created_repo], [@invalid_repo]])
+ expect(repos).to receive(:current_page).and_return(1)
+ expect(repos).to receive(:next_page).and_return(2)
+ expect(repos).to receive(:prev_page).and_return(nil)
+ expect(client).to receive(:repos).and_return(repos)
get :status
diff --git a/spec/controllers/profiles/keys_controller_spec.rb b/spec/controllers/profiles/keys_controller_spec.rb
index 363ed410bc0..ea26bc83353 100644
--- a/spec/controllers/profiles/keys_controller_spec.rb
+++ b/spec/controllers/profiles/keys_controller_spec.rb
@@ -4,7 +4,7 @@ describe Profiles::KeysController do
let(:user) { create(:user) }
describe "#get_keys" do
- describe "non existant user" do
+ describe "non existent user" do
it "does not generally work" do
get :get_keys, username: 'not-existent'
diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb
index 6091185e252..b3c8d6a954e 100644
--- a/spec/controllers/projects/artifacts_controller_spec.rb
+++ b/spec/controllers/projects/artifacts_controller_spec.rb
@@ -47,14 +47,37 @@ describe Projects::ArtifactsController do
context 'when codequality file type is supplied' do
let(:file_type) { 'codequality' }
- before do
- create(:ci_job_artifact, :codequality, job: job)
+ context 'when file is stored locally' do
+ before do
+ create(:ci_job_artifact, :codequality, job: job)
+ end
+
+ it 'sends the codequality report' do
+ expect(controller).to receive(:send_file).with(job.job_artifacts_codequality.file.path, hash_including(disposition: 'attachment')).and_call_original
+
+ download_artifact(file_type: file_type)
+ end
end
- it 'sends the codequality report' do
- expect(controller).to receive(:send_file).with(job.job_artifacts_codequality.file.path, hash_including(disposition: 'attachment')).and_call_original
+ context 'when file is stored remotely' do
+ before do
+ stub_artifacts_object_storage
+ create(:ci_job_artifact, :remote_store, :codequality, job: job)
+ end
+
+ it 'sends the codequality report' do
+ expect(controller).to receive(:redirect_to).and_call_original
- download_artifact(file_type: file_type)
+ download_artifact(file_type: file_type)
+ end
+
+ context 'when proxied' do
+ it 'sends the codequality report' do
+ expect(Gitlab::Workhorse).to receive(:send_url).and_call_original
+
+ download_artifact(file_type: file_type, proxy: true)
+ end
+ end
end
end
end
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index 64b589a6d83..5fdf7f1229d 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -157,7 +157,7 @@ describe Projects::BlobController do
match_line = JSON.parse(response.body).first
- expect(match_line['type']).to eq('context')
+ expect(match_line['type']).to be_nil
end
it 'adds bottom match line when "t"o is less than blob size' do
@@ -177,7 +177,7 @@ describe Projects::BlobController do
match_line = JSON.parse(response.body).last
- expect(match_line['type']).to eq('context')
+ expect(match_line['type']).to be_nil
end
end
end
@@ -331,10 +331,10 @@ describe Projects::BlobController do
expect(response).to redirect_to(
project_new_merge_request_path(
forked_project,
- merge_request_source_branch: "fork-test-1",
merge_request: {
source_project_id: forked_project.id,
target_project_id: project.id,
+ source_branch: "fork-test-1",
target_branch: "master"
}
)
diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb
index 667eaa5e34f..8d503f6ad32 100644
--- a/spec/controllers/projects/boards_controller_spec.rb
+++ b/spec/controllers/projects/boards_controller_spec.rb
@@ -38,9 +38,10 @@ describe Projects::BoardsController do
end
it 'renders template if visited board is not found' do
- visited = double
+ temporary_board = create(:board, project: project)
+ visited = create(:board_project_recent_visit, project: temporary_board.project, board: temporary_board, user: user)
+ temporary_board.delete
- allow(visited).to receive(:board_id).and_return(12)
allow_any_instance_of(Boards::Visits::LatestService).to receive(:execute).and_return(visited)
list_boards
diff --git a/spec/controllers/projects/clusters/applications_controller_spec.rb b/spec/controllers/projects/clusters/applications_controller_spec.rb
index 9e17e392d3d..8106453a775 100644
--- a/spec/controllers/projects/clusters/applications_controller_spec.rb
+++ b/spec/controllers/projects/clusters/applications_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::Clusters::ApplicationsController do
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
index 9201332c5c8..483222363bb 100644
--- a/spec/controllers/projects/clusters_controller_spec.rb
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -1,8 +1,11 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::ClustersController do
include AccessMatchersForController
include GoogleApi::CloudPlatformHelpers
+ include KubernetesHelpers
set(:project) { create(:project) }
@@ -119,7 +122,7 @@ describe Projects::ClustersController do
it 'has new object' do
go
- expect(assigns(:gcp_cluster)).to be_an_instance_of(Clusters::Cluster)
+ expect(assigns(:gcp_cluster)).to be_an_instance_of(Clusters::ClusterPresenter)
end
end
@@ -140,7 +143,7 @@ describe Projects::ClustersController do
it 'has new object' do
go
- expect(assigns(:user_cluster)).to be_an_instance_of(Clusters::Cluster)
+ expect(assigns(:user_cluster)).to be_an_instance_of(Clusters::ClusterPresenter)
end
end
@@ -218,9 +221,9 @@ describe Projects::ClustersController do
describe 'security' do
before do
allow_any_instance_of(described_class)
- .to receive(:token_in_session).and_return('token')
+ .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)
+ .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(
@@ -307,6 +310,11 @@ describe Projects::ClustersController do
end
describe 'security' do
+ before do
+ allow(ClusterPlatformConfigureWorker).to receive(:perform_async)
+ stub_kubeclient_get_namespace('https://kubernetes.example.com', namespace: 'my-namespace')
+ end
+
it { expect { go }.to be_allowed_for(:admin) }
it { expect { go }.to be_allowed_for(:owner).of(project) }
it { expect { go }.to be_allowed_for(:maintainer).of(project) }
@@ -318,14 +326,15 @@ describe Projects::ClustersController do
end
end
- describe 'GET status' do
+ describe 'GET cluster_status' do
let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) }
def go
- get :status, namespace_id: project.namespace,
- project_id: project,
- id: cluster,
- format: :json
+ get :cluster_status,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: cluster,
+ format: :json
end
describe 'functionality' do
@@ -359,9 +368,10 @@ describe Projects::ClustersController do
let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
def go
- get :show, namespace_id: project.namespace,
- project_id: project,
- id: cluster
+ get :show,
+ namespace_id: project.namespace,
+ project_id: project,
+ id: cluster
end
describe 'functionality' do
@@ -386,7 +396,20 @@ describe Projects::ClustersController do
end
describe 'PUT update' do
- let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+ def go(format: :html)
+ put :update, params.merge(namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: cluster,
+ format: format
+ )
+ end
+
+ before do
+ allow(ClusterPlatformConfigureWorker).to receive(:perform_async)
+ stub_kubeclient_get_namespace('https://kubernetes.example.com', namespace: 'my-namespace')
+ end
+
+ let(:cluster) { create(:cluster, :provided_by_user, projects: [project]) }
let(:params) do
{
@@ -400,113 +423,59 @@ describe Projects::ClustersController do
}
end
- def go(format: :html)
- put :update, params.merge(namespace_id: project.namespace,
- project_id: project,
- id: cluster,
- format: format
- )
- end
-
- context 'when cluster is provided by GCP' do
- it "updates and redirects back to show page" do
- go
-
- cluster.reload
- expect(response).to redirect_to(project_cluster_path(project, cluster))
- expect(flash[:notice]).to eq('Kubernetes cluster was successfully updated.')
- expect(cluster.enabled).to be_falsey
- end
-
- it "does not change cluster name" do
- go
-
- cluster.reload
- expect(cluster.name).to eq('test-cluster')
- end
-
- context 'when cluster is being created' do
- let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) }
-
- it "rejects changes" do
- go
+ it "updates and redirects back to show page" do
+ go
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template(:show)
- expect(cluster.enabled).to be_truthy
- end
- end
+ cluster.reload
+ expect(response).to redirect_to(project_cluster_path(project, 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.platform_kubernetes.namespace).to eq('my-namespace')
end
- context 'when cluster is provided by user' do
- let(:cluster) { create(:cluster, :provided_by_user, projects: [project]) }
-
- let(:params) do
- {
- cluster: {
- enabled: false,
- name: 'my-new-cluster-name',
- platform_kubernetes_attributes: {
- namespace: 'my-namespace'
- }
- }
- }
- end
-
- it "updates and redirects back to show page" do
- go
-
- cluster.reload
- expect(response).to redirect_to(project_cluster_path(project, 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.platform_kubernetes.namespace).to eq('my-namespace')
- 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',
- platform_kubernetes_attributes: {
- namespace: 'my-namespace'
- }
+ 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',
+ platform_kubernetes_attributes: {
+ namespace: 'my-namespace'
}
}
- end
+ }
+ end
- it "updates and redirects back to show page" do
- go(format: :json)
+ it "updates and redirects back to show page" do
+ go(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')
- expect(cluster.platform_kubernetes.namespace).to eq('my-namespace')
- end
+ 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')
+ expect(cluster.platform_kubernetes.namespace).to eq('my-namespace')
end
+ end
- context 'when invalid parameters are used' do
- let(:params) do
- {
- cluster: {
- enabled: false,
- platform_kubernetes_attributes: {
- namespace: 'my invalid namespace #@'
- }
+ context 'when invalid parameters are used' do
+ let(:params) do
+ {
+ cluster: {
+ enabled: false,
+ platform_kubernetes_attributes: {
+ namespace: 'my invalid namespace #@'
}
}
- end
+ }
+ end
- it "rejects changes" do
- go(format: :json)
+ it "rejects changes" do
+ go(format: :json)
- expect(response).to have_http_status(:bad_request)
- end
+ expect(response).to have_http_status(:bad_request)
end
end
end
@@ -530,9 +499,10 @@ describe Projects::ClustersController do
let!(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, projects: [project]) }
def go
- delete :destroy, namespace_id: project.namespace,
- project_id: project,
- id: cluster
+ delete :destroy,
+ namespace_id: project.namespace,
+ project_id: project,
+ id: cluster
end
describe 'functionality' do
@@ -591,4 +561,10 @@ describe Projects::ClustersController do
it { expect { go }.to be_denied_for(:external) }
end
end
+
+ context 'no project_id param' do
+ it 'does not respond to any action without project_id param' do
+ expect { get :index }.to raise_error(ActionController::UrlGenerationError)
+ end
+ end
end
diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb
index 9e149bc4c3c..e34fdee62d6 100644
--- a/spec/controllers/projects/commit_controller_spec.rb
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -356,6 +356,7 @@ describe Projects::CommitController do
expect(response).to be_ok
expect(JSON.parse(response.body)['pipelines']).not_to be_empty
expect(JSON.parse(response.body)['count']['all']).to eq 1
+ expect(response).to include_pagination_headers
end
end
end
diff --git a/spec/controllers/projects/deployments_controller_spec.rb b/spec/controllers/projects/deployments_controller_spec.rb
index d1c960e895d..5b7da81b6a1 100644
--- a/spec/controllers/projects/deployments_controller_spec.rb
+++ b/spec/controllers/projects/deployments_controller_spec.rb
@@ -15,9 +15,9 @@ describe Projects::DeploymentsController do
describe 'GET #index' do
it 'returns list of deployments from last 8 hours' do
- create(:deployment, environment: environment, created_at: 9.hours.ago)
- create(:deployment, environment: environment, created_at: 7.hours.ago)
- create(:deployment, environment: environment)
+ create(:deployment, :success, environment: environment, created_at: 9.hours.ago)
+ create(:deployment, :success, environment: environment, created_at: 7.hours.ago)
+ create(:deployment, :success, environment: environment)
get :index, deployment_params(after: 8.hours.ago)
@@ -27,7 +27,7 @@ describe Projects::DeploymentsController do
end
it 'returns a list with deployments information' do
- create(:deployment, environment: environment)
+ create(:deployment, :success, environment: environment)
get :index, deployment_params
@@ -37,7 +37,7 @@ describe Projects::DeploymentsController do
end
describe 'GET #metrics' do
- let(:deployment) { create(:deployment, project: project, environment: environment) }
+ let(:deployment) { create(:deployment, :success, project: project, environment: environment) }
before do
allow(controller).to receive(:deployment).and_return(deployment)
@@ -110,7 +110,7 @@ describe Projects::DeploymentsController do
end
describe 'GET #additional_metrics' do
- let(:deployment) { create(:deployment, project: project, environment: environment) }
+ let(:deployment) { create(:deployment, :success, project: project, environment: environment) }
before do
allow(controller).to receive(:deployment).and_return(deployment)
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index b86029a4baf..bc17331f531 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -216,7 +216,7 @@ describe Projects::EnvironmentsController do
expect(response).to have_gitlab_http_status(200)
end
- it 'loads the terminals for the enviroment' do
+ it 'loads the terminals for the environment' do
expect_any_instance_of(Environment).to receive(:terminals)
get :terminal, environment_params
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index 2023d4b0bd0..da3d658d061 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -152,11 +152,33 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('job/job_details')
expect(json_response['raw_path']).to match(%r{jobs/\d+/raw\z})
- expect(json_response.dig('merge_request', 'path')).to match(%r{merge_requests/\d+\z})
+ expect(json_response['merge_request']['path']).to match(%r{merge_requests/\d+\z})
expect(json_response['new_issue_path']).to include('/issues/new')
end
end
+ context 'when job is running' do
+ context 'job is cancelable' do
+ let(:job) { create(:ci_build, :running, pipeline: pipeline) }
+
+ it 'cancel_path is present with correct redirect' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('job/job_details')
+ expect(json_response['cancel_path']).to include(CGI.escape(json_response['build_path']))
+ end
+ end
+
+ context 'with web terminal' do
+ let(:job) { create(:ci_build, :running, :with_runner_session, pipeline: pipeline) }
+
+ it 'exposes the terminal path' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('job/job_details')
+ expect(json_response['terminal_path']).to match(%r{/terminal})
+ end
+ end
+ end
+
context 'when job has artifacts' do
context 'with not expiry date' do
let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
@@ -185,16 +207,6 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
end
- context 'when job has terminal' do
- let(:job) { create(:ci_build, :running, :with_runner_session, pipeline: pipeline) }
-
- it 'exposes the terminal path' do
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('job/job_details')
- expect(json_response['terminal_path']).to match(%r{/terminal})
- end
- end
-
context 'when job passed with no trace' do
let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
@@ -219,7 +231,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
context 'with deployment' do
let(:merge_request) { create(:merge_request, source_project: project) }
let(:environment) { create(:environment, project: project, name: 'staging', state: :available) }
- let(:job) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) }
+ let(:job) { create(:ci_build, :running, environment: environment.name, pipeline: pipeline) }
it 'exposes the deployment information' do
expect(response).to have_gitlab_http_status(:ok)
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 7b0459e0325..e62523c65c9 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -563,6 +563,7 @@ describe Projects::MergeRequestsController do
it 'responds with serialized pipelines' do
expect(json_response['pipelines']).not_to be_empty
expect(json_response['count']['all']).to eq 1
+ expect(response).to include_pagination_headers
end
end
@@ -754,7 +755,7 @@ describe Projects::MergeRequestsController do
let(:environment) { create(:environment, project: forked) }
let(:pipeline) { create(:ci_pipeline, sha: sha, project: forked) }
let(:build) { create(:ci_build, pipeline: pipeline) }
- let!(:deployment) { create(:deployment, environment: environment, sha: sha, ref: 'master', deployable: build) }
+ let!(:deployment) { create(:deployment, :succeed, environment: environment, sha: sha, ref: 'master', deployable: build) }
let(:merge_request) do
create(:merge_request, source_project: forked, target_project: project, target_branch: 'master', head_pipeline: pipeline)
@@ -779,14 +780,14 @@ describe Projects::MergeRequestsController do
let(:merge_commit_sha) { project.repository.merge(user, forked.commit.id, merge_request, "merged in test") }
let(:post_merge_pipeline) { create(:ci_pipeline, sha: merge_commit_sha, project: project) }
let(:post_merge_build) { create(:ci_build, pipeline: post_merge_pipeline) }
- let!(:source_deployment) { create(:deployment, environment: source_environment, sha: merge_commit_sha, ref: 'master', deployable: post_merge_build) }
+ let!(:source_deployment) { create(:deployment, :succeed, environment: source_environment, sha: merge_commit_sha, ref: 'master', deployable: post_merge_build) }
before do
merge_request.update!(merge_commit_sha: merge_commit_sha)
merge_request.mark_as_merged!
end
- it 'returns the enviroment on the source project' do
+ it 'returns the environment on the source project' do
get_ci_environments_status(environment_target: 'merge_commit')
expect(response).to have_gitlab_http_status(:ok)
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index 3190f1ce9d4..ccd4fc4db3a 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Projects::MilestonesController do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:milestone) { create(:milestone, project: project) }
let(:issue) { create(:issue, project: project, milestone: milestone) }
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 85ba7d4097d..90754319f05 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -27,6 +27,12 @@ FactoryBot.define do
pipeline factory: :ci_pipeline
+ trait :degenerated do
+ commands nil
+ options nil
+ yaml_variables nil
+ end
+
trait :started do
started_at 'Di 29. Okt 09:51:28 CET 2013'
end
@@ -94,6 +100,30 @@ FactoryBot.define do
url: 'http://staging.example.com/$CI_JOB_NAME' }
end
+ trait :deploy_to_production do
+ environment 'production'
+
+ options environment: { name: 'production',
+ url: 'http://prd.example.com/$CI_JOB_NAME' }
+ end
+
+ trait :start_review_app do
+ environment 'review/$CI_COMMIT_REF_NAME'
+
+ options environment: { name: 'review/$CI_COMMIT_REF_NAME',
+ url: 'http://staging.example.com/$CI_JOB_NAME',
+ on_stop: 'stop_review_app' }
+ end
+
+ trait :stop_review_app do
+ name 'stop_review_app'
+ environment 'review/$CI_COMMIT_REF_NAME'
+
+ options environment: { name: 'review/$CI_COMMIT_REF_NAME',
+ url: 'http://staging.example.com/$CI_JOB_NAME',
+ action: 'stop' }
+ end
+
trait :allowed_to_fail do
allow_failure true
end
diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb
index 3c9ca22a051..ff65c76cf26 100644
--- a/spec/factories/clusters/applications/helm.rb
+++ b/spec/factories/clusters/applications/helm.rb
@@ -57,6 +57,11 @@ FactoryBot.define do
cluster factory: %i(cluster with_installed_helm provided_by_gcp)
end
+ factory :clusters_applications_knative, class: Clusters::Applications::Knative do
+ hostname 'example.com'
+ cluster factory: %i(cluster with_installed_helm provided_by_gcp)
+ end
+
factory :clusters_applications_jupyter, class: Clusters::Applications::Jupyter do
oauth_application factory: :oauth_application
cluster factory: %i(cluster with_installed_helm provided_by_gcp project)
diff --git a/spec/factories/clusters/kubernetes_namespaces.rb b/spec/factories/clusters/kubernetes_namespaces.rb
index 6fdada75a3d..3a4f5193550 100644
--- a/spec/factories/clusters/kubernetes_namespaces.rb
+++ b/spec/factories/clusters/kubernetes_namespaces.rb
@@ -2,8 +2,18 @@
FactoryBot.define do
factory :cluster_kubernetes_namespace, class: Clusters::KubernetesNamespace do
- cluster
- project
- cluster_project
+ association :cluster, :project, :provided_by_gcp
+ namespace { |n| "environment#{n}" }
+
+ after(:build) do |kubernetes_namespace|
+ cluster_project = kubernetes_namespace.cluster.cluster_project
+
+ kubernetes_namespace.project = cluster_project.project
+ kubernetes_namespace.cluster_project = cluster_project
+ end
+
+ trait :with_token do
+ service_account_token { FFaker::Lorem.characters(10) }
+ end
end
end
diff --git a/spec/factories/clusters/platforms/kubernetes.rb b/spec/factories/clusters/platforms/kubernetes.rb
index 4a0d1b181ea..8169c457ab7 100644
--- a/spec/factories/clusters/platforms/kubernetes.rb
+++ b/spec/factories/clusters/platforms/kubernetes.rb
@@ -10,7 +10,7 @@ FactoryBot.define do
username 'xxxxxx'
password 'xxxxxx'
- after(:create) do |platform_kubernetes, evaluator|
+ before(:create) do |platform_kubernetes, evaluator|
pem_file = File.expand_path(Rails.root.join('spec/fixtures/clusters/sample_cert.pem'))
platform_kubernetes.ca_cert = File.read(pem_file)
end
diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb
index 90d6a338479..011c98599a3 100644
--- a/spec/factories/deployments.rb
+++ b/spec/factories/deployments.rb
@@ -21,5 +21,31 @@ FactoryBot.define do
sha { TestEnv::BRANCH_SHA['pages-deploy'] }
ref 'pages-deploy'
end
+
+ trait :running do
+ status :running
+ end
+
+ trait :success do
+ status :success
+ finished_at { Time.now }
+ end
+
+ trait :failed do
+ status :failed
+ finished_at { Time.now }
+ end
+
+ trait :canceled do
+ status :canceled
+ finished_at { Time.now }
+ end
+
+ # This trait hooks the state maechine's events
+ trait :succeed do
+ after(:create) do |deployment, evaluator|
+ deployment.succeed!
+ end
+ end
end
end
diff --git a/spec/factories/environments.rb b/spec/factories/environments.rb
index b5db57d5148..9d9e3d693b8 100644
--- a/spec/factories/environments.rb
+++ b/spec/factories/environments.rb
@@ -22,6 +22,7 @@ FactoryBot.define do
pipeline: pipeline)
deployment = create(:deployment,
+ :success,
environment: environment,
project: environment.project,
deployable: deployable,
diff --git a/spec/factories/merge_request_diff_files.rb b/spec/factories/merge_request_diff_files.rb
new file mode 100644
index 00000000000..469a7a0ac8d
--- /dev/null
+++ b/spec/factories/merge_request_diff_files.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :merge_request_diff_file do
+ association :merge_request_diff
+
+ relative_order 0
+ new_file true
+ renamed_file false
+ deleted_file false
+ too_large false
+ a_mode 0
+ b_mode 100644
+ new_path 'foo'
+ old_path 'foo'
+ diff ''
+ binary false
+
+ trait :new_file do
+ relative_order 0
+ new_file true
+ renamed_file false
+ deleted_file false
+ too_large false
+ a_mode 0
+ b_mode 100644
+ new_path 'foo'
+ old_path 'foo'
+ diff ''
+ binary false
+ end
+
+ trait :renamed_file do
+ relative_order 662
+ new_file false
+ renamed_file true
+ deleted_file false
+ too_large false
+ a_mode 100644
+ b_mode 100644
+ new_path 'bar'
+ old_path 'baz'
+ diff ''
+ binary false
+ end
+ end
+end
diff --git a/spec/factories/merge_request_diffs.rb b/spec/factories/merge_request_diffs.rb
new file mode 100644
index 00000000000..e7b51189538
--- /dev/null
+++ b/spec/factories/merge_request_diffs.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :merge_request_diff do
+ association :merge_request
+ state :collected
+ commits_count 1
+
+ base_commit_sha { Digest::SHA1.hexdigest(SecureRandom.hex) }
+ head_commit_sha { Digest::SHA1.hexdigest(SecureRandom.hex) }
+ start_commit_sha { Digest::SHA1.hexdigest(SecureRandom.hex) }
+ end
+end
diff --git a/spec/factories/services.rb b/spec/factories/services.rb
index 0d4fd49bf3a..5be56a49903 100644
--- a/spec/factories/services.rb
+++ b/spec/factories/services.rb
@@ -46,6 +46,17 @@ FactoryBot.define do
)
end
+ factory :jira_cloud_service, class: JiraService do
+ project
+ active true
+ properties(
+ url: 'https://mysite.atlassian.net',
+ username: 'jira_user',
+ password: 'my-secret-password',
+ project_key: 'jira-key'
+ )
+ end
+
factory :hipchat_service do
project
type 'HipchatService'
diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb
index 96dfde2e08c..735ca60f7da 100644
--- a/spec/features/admin/admin_groups_spec.rb
+++ b/spec/features/admin/admin_groups_spec.rb
@@ -53,13 +53,33 @@ describe 'Admin Groups' do
expect_selected_visibility(internal)
end
- it 'when entered in group path, it auto filled the group name', :js do
+ it 'when entered in group name, it auto filled the group path', :js do
visit admin_groups_path
click_link "New group"
- group_path = 'gitlab'
+ group_name = 'gitlab'
+ fill_in 'group_name', with: group_name
+ path_field = find('input#group_path')
+ expect(path_field.value).to eq group_name
+ end
+
+ it 'auto populates the group path with the group name', :js do
+ visit admin_groups_path
+ click_link "New group"
+ group_name = 'my gitlab project'
+ fill_in 'group_name', with: group_name
+ path_field = find('input#group_path')
+ expect(path_field.value).to eq 'my-gitlab-project'
+ end
+
+ it 'when entering in group path, group name does not change anymore', :js do
+ visit admin_groups_path
+ click_link "New group"
+ group_path = 'my-gitlab-project'
+ group_name = 'My modified gitlab project'
fill_in 'group_path', with: group_path
- name_field = find('input#group_name')
- expect(name_field.value).to eq group_path
+ fill_in 'group_name', with: group_name
+ path_field = find('input#group_path')
+ expect(path_field.value).to eq 'my-gitlab-project'
end
end
diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb
index eebc987499d..030993462b5 100644
--- a/spec/features/boards/add_issues_modal_spec.rb
+++ b/spec/features/boards/add_issues_modal_spec.rb
@@ -160,7 +160,7 @@ describe 'Issue Boards add issue modal', :js do
it 'changes button text with plural' do
page.within('.add-issues-modal') do
- all('.board-card .board-card-number').each do |el|
+ all('.board-card .js-board-card-number-container').each do |el|
el.click
end
diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb
index ec0ca21450a..21779336559 100644
--- a/spec/features/boards/issue_ordering_spec.rb
+++ b/spec/features/boards/issue_ordering_spec.rb
@@ -78,7 +78,7 @@ describe 'Issue Boards', :js do
end
it 'moves from bottom to top' do
- drag(from_index: 2, to_index: 0)
+ drag(from_index: 2, to_index: 0, duration: 1020)
wait_for_requests
@@ -130,7 +130,7 @@ describe 'Issue Boards', :js do
end
it 'moves to bottom of another list' do
- drag(list_from_index: 1, list_to_index: 2, to_index: 2)
+ drag(list_from_index: 1, list_to_index: 2, to_index: 2, duration: 1020)
wait_for_requests
diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb
index aa3ca8923ff..a1f93bd3fbd 100644
--- a/spec/features/calendar_spec.rb
+++ b/spec/features/calendar_spec.rb
@@ -153,7 +153,7 @@ describe 'Contributions Calendar', :js do
include_context 'visit user page'
it 'displays calendar activity log' do
- expect(find('.tab-pane#activity .content_list .event-note')).to have_content issue_title
+ expect(find('.tab-pane#activity .content_list .event-target-title')).to have_content issue_title
end
end
end
diff --git a/spec/features/dashboard/archived_projects_spec.rb b/spec/features/dashboard/archived_projects_spec.rb
index 6a0cd848345..d31df322d10 100644
--- a/spec/features/dashboard/archived_projects_spec.rb
+++ b/spec/features/dashboard/archived_projects_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe 'Dashboard Archived Project' do
expect(page).not_to have_content(project.name)
end
- it 'searchs archived projects', :js do
+ it 'searches archived projects', :js do
click_button 'Last updated'
click_link 'Show archived projects'
diff --git a/spec/features/dashboard/group_spec.rb b/spec/features/dashboard/group_spec.rb
index e57fcde8b2c..259f220c68b 100644
--- a/spec/features/dashboard/group_spec.rb
+++ b/spec/features/dashboard/group_spec.rb
@@ -14,15 +14,15 @@ RSpec.describe 'Dashboard Group' do
it 'creates new group', :js do
visit dashboard_groups_path
find('.btn-success').click
- new_path = 'Samurai'
+ new_name = 'Samurai'
new_description = 'Tokugawa Shogunate'
- fill_in 'group_path', with: new_path
+ fill_in 'group_name', with: new_name
fill_in 'group_description', with: new_description
click_button 'Create group'
- expect(current_path).to eq group_path(Group.find_by(name: new_path))
- expect(page).to have_content(new_path)
+ expect(current_path).to eq group_path(Group.find_by(name: new_name))
+ expect(page).to have_content(new_name)
expect(page).to have_content(new_description)
end
end
diff --git a/spec/features/dashboard/project_member_activity_index_spec.rb b/spec/features/dashboard/project_member_activity_index_spec.rb
index 498775acff3..16919fe63ad 100644
--- a/spec/features/dashboard/project_member_activity_index_spec.rb
+++ b/spec/features/dashboard/project_member_activity_index_spec.rb
@@ -14,14 +14,15 @@ describe 'Project member activity', :js do
wait_for_requests
end
- subject { page.find(".event-title").text }
-
context 'when a user joins the project' do
before do
visit_activities_and_wait_with_event(Event::JOINED)
end
- it { is_expected.to eq("#{user.name} joined project") }
+ it "presents the correct message" do
+ expect(page.find('.event-user-info').text).to eq("#{user.name} #{user.to_reference}")
+ expect(page.find('.event-title').text).to eq("joined project")
+ end
end
context 'when a user leaves the project' do
@@ -29,7 +30,10 @@ describe 'Project member activity', :js do
visit_activities_and_wait_with_event(Event::LEFT)
end
- it { is_expected.to eq("#{user.name} left project") }
+ it "presents the correct message" do
+ expect(page.find('.event-user-info').text).to eq("#{user.name} #{user.to_reference}")
+ expect(page.find('.event-title').text).to eq("left project")
+ end
end
context 'when a users membership expires for the project' do
@@ -38,8 +42,8 @@ describe 'Project member activity', :js do
end
it "presents the correct message" do
- message = "#{user.name} removed due to membership expiration from project"
- is_expected.to eq(message)
+ expect(page.find('.event-user-info').text).to eq("#{user.name} #{user.to_reference}")
+ expect(page.find('.event-title').text).to eq("removed due to membership expiration from project")
end
end
end
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index 0a24c5e906a..975b7944741 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -147,12 +147,10 @@ describe 'Dashboard Projects' do
end
context 'last push widget', :use_clean_rails_memory_store_caching do
- let(:ref) { "feature" }
-
before do
event = create(:push_event, project: project, author: user)
- create(:push_event_payload, event: event, ref: ref, action: :created)
+ create(:push_event_payload, event: event, ref: 'feature', action: :created)
Users::LastPushEventService.new(user).cache_last_push_event(event)
@@ -167,9 +165,9 @@ describe 'Dashboard Projects' do
end
expect(page).to have_selector('.merge-request-form')
- expect(current_path).to eq project_new_merge_request_path(project, merge_request_source_branch: ref)
+ expect(current_path).to eq project_new_merge_request_path(project)
expect(find('#merge_request_target_project_id', visible: false).value).to eq project.id.to_s
- expect(find('input#merge_request_source_branch', visible: false).value).to eq ref
+ expect(find('input#merge_request_source_branch', visible: false).value).to eq 'feature'
expect(find('input#merge_request_target_branch', visible: false).value).to eq 'master'
end
end
diff --git a/spec/features/explore/new_menu_spec.rb b/spec/features/explore/new_menu_spec.rb
index 11f05b6d220..259f22139ef 100644
--- a/spec/features/explore/new_menu_spec.rb
+++ b/spec/features/explore/new_menu_spec.rb
@@ -29,7 +29,7 @@ describe 'Top Plus Menu', :js do
click_topmenuitem("New group")
- expect(page).to have_content('Group path')
+ expect(page).to have_content('Group URL')
expect(page).to have_content('Group name')
end
@@ -79,7 +79,7 @@ describe 'Top Plus Menu', :js do
click_topmenuitem("New subgroup")
- expect(page).to have_content('Group path')
+ expect(page).to have_content('Group URL')
expect(page).to have_content('Group name')
end
diff --git a/spec/features/groups/board_sidebar_spec.rb b/spec/features/groups/board_sidebar_spec.rb
new file mode 100644
index 00000000000..9f597efa7b7
--- /dev/null
+++ b/spec/features/groups/board_sidebar_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe 'Group Issue Boards', :js do
+ include BoardHelpers
+
+ let(:group) { create(:group) }
+ let(:user) { create(:group_member, user: create(:user), group: group ).user }
+ let!(:project_1) { create(:project, :public, group: group) }
+ let!(:project_2) { create(:project, :public, group: group) }
+ let!(:project_1_label) { create(:label, project: project_1, name: 'Development 1') }
+ let!(:project_2_label) { create(:label, project: project_2, name: 'Development 2') }
+ let!(:group_label) { create(:group_label, title: 'Bug', description: 'Fusce consequat', group: group) }
+ let!(:issue_1) { create(:labeled_issue, project: project_1, relative_position: 1) }
+ let!(:issue_2) { create(:labeled_issue, project: project_2, relative_position: 2) }
+ let(:board) { create(:board, group: group) }
+ let!(:list) { create(:list, board: board, label: project_1_label, position: 0) }
+ let(:card) { find('.board:nth-child(1)').first('.board-card') }
+
+ before do
+ sign_in(user)
+
+ visit group_board_path(group, board)
+ wait_for_requests
+ end
+
+ context 'labels' do
+ it 'only shows valid labels for the issue project and group' do
+ click_card(card)
+
+ page.within('.labels') do
+ click_link 'Edit'
+
+ wait_for_requests
+
+ page.within('.selectbox') do
+ expect(page).to have_content(project_1_label.title)
+ expect(page).to have_content(group_label.title)
+ expect(page).not_to have_content(project_2_label.title)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index 63aa26cf5fd..d01fc04311a 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -1,13 +1,15 @@
require 'spec_helper'
describe 'Group' do
+ let(:user) { create(:admin) }
+
before do
- sign_in(create(:admin))
+ sign_in(user)
end
matcher :have_namespace_error_message do
match do |page|
- page.has_content?("Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-' or end in '.', '.git' or '.atom'.")
+ page.has_content?("Group URL can contain only letters, digits, '_', '-' and '.'. Cannot start with '-' or end in '.', '.git' or '.atom'.")
end
end
@@ -16,9 +18,27 @@ describe 'Group' do
visit new_group_path
end
+ describe 'as a non-admin' do
+ let(:user) { create(:user) }
+
+ it 'creates a group and persists visibility radio selection', :js do
+ stub_application_setting(default_group_visibility: :private)
+
+ fill_in 'Group name', with: 'test-group'
+ find("input[name='group[visibility_level]'][value='#{Gitlab::VisibilityLevel::PUBLIC}']").click
+ click_button 'Create group'
+
+ group = Group.find_by(name: 'test-group')
+
+ expect(group.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ expect(current_path).to eq(group_path(group))
+ expect(page).to have_selector '.visibility-icon .fa-globe'
+ end
+ end
+
describe 'with space in group path' do
it 'renders new group form with validation errors' do
- fill_in 'Group path', with: 'space group'
+ fill_in 'Group URL', with: 'space group'
click_button 'Create group'
expect(current_path).to eq(groups_path)
@@ -28,7 +48,7 @@ describe 'Group' do
describe 'with .atom at end of group path' do
it 'renders new group form with validation errors' do
- fill_in 'Group path', with: 'atom_group.atom'
+ fill_in 'Group URL', with: 'atom_group.atom'
click_button 'Create group'
expect(current_path).to eq(groups_path)
@@ -38,7 +58,7 @@ describe 'Group' do
describe 'with .git at end of group path' do
it 'renders new group form with validation errors' do
- fill_in 'Group path', with: 'git_group.git'
+ fill_in 'Group URL', with: 'git_group.git'
click_button 'Create group'
expect(current_path).to eq(groups_path)
@@ -94,7 +114,8 @@ describe 'Group' do
end
it 'creates a nested group' do
- fill_in 'Group path', with: 'bar'
+ fill_in 'Group name', with: 'bar'
+ fill_in 'Group URL', with: 'bar'
click_button 'Create group'
expect(current_path).to eq(group_path('foo/bar'))
@@ -112,7 +133,8 @@ describe 'Group' do
visit new_group_path(group, parent_id: group.id)
- fill_in 'Group path', with: 'bar'
+ fill_in 'Group name', with: 'bar'
+ fill_in 'Group URL', with: 'bar'
click_button 'Create group'
expect(current_path).to eq(group_path('foo/bar'))
diff --git a/spec/features/import/manifest_import_spec.rb b/spec/features/import/manifest_import_spec.rb
index e381d073804..a90cdd8d920 100644
--- a/spec/features/import/manifest_import_spec.rb
+++ b/spec/features/import/manifest_import_spec.rb
@@ -22,7 +22,7 @@ describe 'Import multiple repositories by uploading a manifest file', :js, :post
expect(page).to have_content('https://android-review.googlesource.com/platform/build/blueprint')
end
- it 'imports succesfully imports a project' do
+ it 'imports successfully imports a project' do
visit new_import_manifest_path
attach_file('manifest', Rails.root.join('spec/fixtures/aosp_manifest.xml'))
diff --git a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
index be229e8aa7d..c42fcd92a36 100644
--- a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
@@ -92,7 +92,7 @@ describe 'Dropdown emoji', :js do
it 'shows the most populated emoji at top of dropdown' do
send_keys_to_filtered_search('my-reaction:')
- expect(first('#js-dropdown-my-reaction li')).to have_content(award_emoji_star.name)
+ expect(first('#js-dropdown-my-reaction .filter-dropdown li')).to have_content(award_emoji_star.name)
end
end
@@ -121,13 +121,29 @@ describe 'Dropdown emoji', :js do
send_keys_to_filtered_search(':')
end
+ it 'selects `None`' do
+ find('#js-dropdown-my-reaction .filter-dropdown-item', text: 'None').click
+
+ expect(page).to have_css(js_dropdown_emoji, visible: false)
+ expect_tokens([reaction_token('none', false)])
+ expect_filtered_search_input_empty
+ end
+
+ it 'selects `Any`' do
+ find('#js-dropdown-my-reaction .filter-dropdown-item', text: 'Any').click
+
+ expect(page).to have_css(js_dropdown_emoji, visible: false)
+ expect_tokens([reaction_token('any', false)])
+ expect_filtered_search_input_empty
+ end
+
it 'fills in the my-reaction name' do
click_emoji('thumbsup')
wait_for_requests
expect(page).to have_css(js_dropdown_emoji, visible: false)
- expect_tokens([emoji_token('thumbsup')])
+ expect_tokens([reaction_token('thumbsup')])
expect_filtered_search_input_empty
end
end
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index 593dc6b6690..605860b90cd 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -15,7 +15,7 @@ describe 'GFM autocomplete', :js do
wait_for_requests
end
- it 'updates issue descripton with GFM reference' do
+ it 'updates issue description with GFM reference' do
find('.js-issuable-edit').click
simulate_input('#issue-description', "@#{user.name[0...3]}")
diff --git a/spec/features/issues/user_views_issue_spec.rb b/spec/features/issues/user_views_issue_spec.rb
index 117e5986f29..330b6f0e77a 100644
--- a/spec/features/issues/user_views_issue_spec.rb
+++ b/spec/features/issues/user_views_issue_spec.rb
@@ -1,9 +1,9 @@
require "spec_helper"
describe "User views issue" do
- set(:project) { create(:project_empty_repo, :public) }
- set(:user) { create(:user) }
- set(:issue) { create(:issue, project: project, description: "# Description header", author: user) }
+ let(:project) { create(:project_empty_repo, :public) }
+ let(:user) { create(:user) }
+ let(:issue) { create(:issue, project: project, description: "# Description header", author: user) }
before do
project.add_developer(user)
diff --git a/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb b/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb
index a124c99ecc8..0ccab5b2fac 100644
--- a/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb
+++ b/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb
@@ -9,10 +9,10 @@ describe 'create a merge request, allowing commits from members who can merge to
def visit_new_merge_request
visit project_new_merge_request_path(
source_project,
- merge_request_source_branch: 'fix',
merge_request: {
source_project_id: source_project.id,
target_project_id: target_project.id,
+ source_branch: 'fix',
target_branch: 'master'
})
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 f0d38dc6a0c..d790bdc82ce 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
@@ -114,10 +114,9 @@ describe 'Merge request > User creates image diff notes', :js do
create_image_diff_note
end
- # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
- xit 'shows indicator and avatar badges, and allows collapsing/expanding the discussion notes' do
+ it 'shows indicator and avatar badges, and allows collapsing/expanding the discussion notes' do
indicator = find('.js-image-badge', match: :first)
- badge = find('.image-diff-avatar-link .badge', match: :first)
+ badge = find('.user-avatar-link .badge', match: :first)
expect(indicator).to have_content('1')
expect(badge).to have_content('1')
@@ -157,8 +156,7 @@ describe 'Merge request > User creates image diff notes', :js do
visit project_merge_request_path(project, merge_request)
end
- # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
- xit 'render diff indicators within the image frame' do
+ it 'render diff indicators within the image frame' do
diff_note = create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position)
wait_for_requests
@@ -200,7 +198,6 @@ describe 'Merge request > User creates image diff notes', :js do
def create_image_diff_note
find('.js-add-image-diff-note-button', match: :first).click
- page.all('.js-add-image-diff-note-button')[0].click
find('.diff-content .note-textarea').native.send_keys('image diff test comment')
click_button 'Comment'
wait_for_requests
diff --git a/spec/features/merge_request/user_posts_diff_notes_spec.rb b/spec/features/merge_request/user_posts_diff_notes_spec.rb
index fa148715855..51b78d3e7d1 100644
--- a/spec/features/merge_request/user_posts_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_diff_notes_spec.rb
@@ -85,12 +85,13 @@ describe 'Merge request > User posts diff notes', :js do
# `.line_holder` will be an unfolded line.
let(:line_holder) { first('#a5cc2925ca8258af241be7e5b0381edf30266302 .line_holder') }
- it 'does not allow commenting on the left side' do
- should_not_allow_commenting(line_holder, 'left')
+ it 'allows commenting on the left side' do
+ should_allow_commenting(line_holder, 'left')
end
- it 'does not allow commenting on the right side' do
- should_not_allow_commenting(line_holder, 'right')
+ it 'allows commenting on the right side' do
+ # Automatically shifts comment box to left side.
+ should_allow_commenting(line_holder, 'right')
end
end
end
@@ -147,8 +148,8 @@ describe 'Merge request > User posts diff notes', :js do
# `.line_holder` will be an unfolded line.
let(:line_holder) { first('.line_holder[id="a5cc2925ca8258af241be7e5b0381edf30266302_1_1"]') }
- it 'does not allow commenting' do
- should_not_allow_commenting line_holder
+ it 'allows commenting' do
+ should_allow_commenting line_holder
end
end
diff --git a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
index d3da8cc6752..b58c433bbfe 100644
--- a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
@@ -89,16 +89,17 @@ describe 'Merge request > User sees avatars on diff notes', :js do
page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').send_keys(:return)
- expect(page).to have_selector('img.js-diff-comment-avatar', count: 1)
+ expect(page).to have_selector('.js-diff-comment-avatar img', count: 1)
end
end
it 'shows comment on note avatar' do
page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').send_keys(:return)
-
- expect(first('img.js-diff-comment-avatar')["data-original-title"]).to eq("#{note.author.name}: #{note.note.truncate(17)}")
+ first('.js-diff-comment-avatar img').hover
end
+
+ expect(page).to have_content "#{note.author.name}: #{note.note.truncate(17)}"
end
it 'toggles comments when clicking avatar' do
@@ -109,7 +110,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do
expect(page).not_to have_selector('.notes_holder')
page.within find_line(position.line_code(project.repository)) do
- first('img.js-diff-comment-avatar').click
+ first('.js-diff-comment-avatar img').click
end
expect(page).to have_selector('.notes_holder')
@@ -125,7 +126,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do
wait_for_requests
page.within find_line(position.line_code(project.repository)) do
- expect(page).not_to have_selector('img.js-diff-comment-avatar')
+ expect(page).not_to have_selector('.js-diff-comment-avatar img')
end
end
@@ -143,7 +144,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do
page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').send_keys(:return)
- expect(page).to have_selector('img.js-diff-comment-avatar', count: 2)
+ expect(page).to have_selector('.js-diff-comment-avatar img', count: 2)
end
end
@@ -162,7 +163,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do
page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').send_keys(:return)
- expect(page).to have_selector('img.js-diff-comment-avatar', count: 3)
+ expect(page).to have_selector('.js-diff-comment-avatar img', count: 3)
expect(find('.diff-comments-more-count')).to have_content '+1'
end
end
diff --git a/spec/features/merge_request/user_sees_deployment_widget_spec.rb b/spec/features/merge_request/user_sees_deployment_widget_spec.rb
index a298ead43db..74290c0fff9 100644
--- a/spec/features/merge_request/user_sees_deployment_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_deployment_widget_spec.rb
@@ -1,7 +1,7 @@
require 'rails_helper'
describe 'Merge request > User sees deployment widget', :js do
- describe 'when deployed to an environment' do
+ describe 'when merge request has associated environments' do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, :merged, source_project: project) }
@@ -10,35 +10,79 @@ describe 'Merge request > User sees deployment widget', :js do
let(:ref) { merge_request.target_branch }
let(:sha) { project.commit(ref).id }
let(:pipeline) { create(:ci_pipeline_without_jobs, sha: sha, project: project, ref: ref) }
- let(:build) { create(:ci_build, :success, pipeline: pipeline) }
- let!(:deployment) { create(:deployment, environment: environment, sha: sha, ref: ref, deployable: build) }
let!(:manual) { }
before do
merge_request.update!(merge_commit_sha: sha)
project.add_user(user, role)
sign_in(user)
- visit project_merge_request_path(project, merge_request)
- wait_for_requests
end
- it 'displays that the environment is deployed' do
- wait_for_requests
+ context 'when deployment succeeded' do
+ let(:build) { create(:ci_build, :success, pipeline: pipeline) }
+ let!(:deployment) { create(:deployment, :succeed, environment: environment, sha: sha, ref: ref, deployable: build) }
- expect(page).to have_content("Deployed to #{environment.name}")
- expect(find('.js-deploy-time')['data-original-title']).to eq(deployment.created_at.to_time.in_time_zone.to_s(:medium))
+ it 'displays that the environment is deployed' do
+ visit project_merge_request_path(project, merge_request)
+ wait_for_requests
+
+ expect(page).to have_content("Deployed to #{environment.name}")
+ expect(find('.js-deploy-time')['data-original-title']).to eq(deployment.created_at.to_time.in_time_zone.to_s(:medium))
+ end
+ end
+
+ context 'when deployment failed' do
+ let(:build) { create(:ci_build, :failed, pipeline: pipeline) }
+ let!(:deployment) { create(:deployment, :failed, environment: environment, sha: sha, ref: ref, deployable: build) }
+
+ it 'displays that the deployment failed' do
+ visit project_merge_request_path(project, merge_request)
+ wait_for_requests
+
+ expect(page).to have_content("Failed to deploy to #{environment.name}")
+ expect(page).not_to have_css('.js-deploy-time')
+ end
+ end
+
+ context 'when deployment running' do
+ let(:build) { create(:ci_build, :running, pipeline: pipeline) }
+ let!(:deployment) { create(:deployment, :running, environment: environment, sha: sha, ref: ref, deployable: build) }
+
+ it 'displays that the running deployment' do
+ visit project_merge_request_path(project, merge_request)
+ wait_for_requests
+
+ expect(page).to have_content("Deploying to #{environment.name}")
+ expect(page).not_to have_css('.js-deploy-time')
+ end
+ end
+
+ context 'when deployment will happen' do
+ let(:build) { create(:ci_build, :created, pipeline: pipeline) }
+ let!(:deployment) { create(:deployment, environment: environment, sha: sha, ref: ref, deployable: build) }
+
+ it 'displays that the environment name' do
+ visit project_merge_request_path(project, merge_request)
+ wait_for_requests
+
+ expect(page).to have_content("Deploying to #{environment.name}")
+ expect(page).not_to have_css('.js-deploy-time')
+ end
end
context 'with stop action' do
+ let(:build) { create(:ci_build, :success, pipeline: pipeline) }
+ let!(:deployment) { create(:deployment, :succeed, environment: environment, sha: sha, ref: ref, deployable: build) }
let(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
before do
deployment.update!(on_stop: manual.name)
+ visit project_merge_request_path(project, merge_request)
wait_for_requests
end
it 'does start build when stop button clicked' do
- accept_confirm { click_button('Stop environment') }
+ accept_confirm { find('.js-stop-env').click }
expect(page).to have_content('close_app')
end
@@ -47,7 +91,7 @@ describe 'Merge request > User sees deployment widget', :js do
let(:role) { :reporter }
it 'does not show stop button' do
- expect(page).not_to have_button('Stop environment')
+ expect(page).not_to have_selector('.js-stop-env')
end
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 d2003b61b2a..582be101399 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -20,10 +20,10 @@ describe 'Merge request > User sees merge widget', :js do
before do
visit project_new_merge_request_path(
project,
- merge_request_source_branch: 'feature',
merge_request: {
source_project_id: project.id,
target_project_id: project.id,
+ source_branch: 'feature',
target_branch: 'master'
})
end
@@ -45,7 +45,8 @@ describe 'Merge request > User sees merge widget', :js do
let(:build) { create(:ci_build, :success, pipeline: pipeline) }
let!(:deployment) do
- create(:deployment, environment: environment,
+ create(:deployment, :succeed,
+ environment: environment,
ref: merge_request.source_branch,
deployable: build,
sha: sha)
@@ -179,7 +180,7 @@ describe 'Merge request > User sees merge widget', :js do
# Wait for the `ci_status` and `merge_check` requests
wait_for_requests
- expect(page).to have_text('Could not connect to the CI server. Please check your settings and try again')
+ expect(page).to have_text("Could not retrieve the pipeline status. For troubleshooting steps, read the documentation.")
end
end
diff --git a/spec/features/merge_request/user_sees_pipelines_spec.rb b/spec/features/merge_request/user_sees_pipelines_spec.rb
index 45cccbee63e..8faddee4daa 100644
--- a/spec/features/merge_request/user_sees_pipelines_spec.rb
+++ b/spec/features/merge_request/user_sees_pipelines_spec.rb
@@ -41,8 +41,7 @@ describe 'Merge request > User sees pipelines', :js do
visit project_merge_request_path(project, merge_request)
wait_for_requests
- expect(page.find('.ci-widget')).to have_content(
- 'Could not connect to the CI server. Please check your settings and try again')
+ expect(page.find('.ci-widget')).to have_text("Could not retrieve the pipeline status. For troubleshooting steps, read the documentation.")
end
end
diff --git a/spec/features/merge_request/user_sees_wip_help_message_spec.rb b/spec/features/merge_request/user_sees_wip_help_message_spec.rb
index 6dfc819fe8a..92cc73ddf1f 100644
--- a/spec/features/merge_request/user_sees_wip_help_message_spec.rb
+++ b/spec/features/merge_request/user_sees_wip_help_message_spec.rb
@@ -13,10 +13,10 @@ describe 'Merge request > User sees WIP help message' do
it 'shows a specific WIP hint' do
visit project_new_merge_request_path(
project,
- merge_request_source_branch: 'wip',
merge_request: {
source_project_id: project.id,
target_project_id: project.id,
+ source_branch: 'wip',
target_branch: 'master'
})
@@ -32,10 +32,10 @@ describe 'Merge request > User sees WIP help message' do
it 'shows the regular WIP message' do
visit project_new_merge_request_path(
project,
- merge_request_source_branch: 'fix',
merge_request: {
source_project_id: project.id,
target_project_id: project.id,
+ source_branch: 'fix',
target_branch: 'master'
})
diff --git a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
index 147544740dc..ae41cf90576 100644
--- a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
+++ b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
@@ -109,13 +109,13 @@ describe 'Merge request > User selects branches for new MR', :js do
end
it 'populates source branch button' do
- visit project_new_merge_request_path(project, change_branches: true, merge_request_source_branch: 'fix', merge_request: { target_branch: 'master' })
+ visit project_new_merge_request_path(project, change_branches: true, merge_request: { target_branch: 'master', source_branch: 'fix' })
expect(find('.js-source-branch')).to have_content('fix')
end
it 'allows to change the diff view' do
- visit project_new_merge_request_path(project, merge_request_source_branch: 'fix', merge_request: { target_branch: 'master' })
+ visit project_new_merge_request_path(project, merge_request: { target_branch: 'master', source_branch: 'fix' })
click_link 'Changes'
@@ -131,7 +131,7 @@ describe 'Merge request > User selects branches for new MR', :js do
end
it 'does not allow non-existing branches' do
- visit project_new_merge_request_path(project, merge_request_source_branch: 'non-exist-source', merge_request: { target_branch: 'non-exist-target' })
+ visit project_new_merge_request_path(project, merge_request: { target_branch: 'non-exist-target', source_branch: 'non-exist-source' })
expect(page).to have_content('The form contains the following errors')
expect(page).to have_content('Source branch "non-exist-source" does not exist')
@@ -140,7 +140,7 @@ describe 'Merge request > User selects branches for new MR', :js do
context 'when a branch contains commits that both delete and add the same image' do
it 'renders the diff successfully' do
- visit project_new_merge_request_path(project, merge_request_source_branch: 'deleted-image-test', merge_request: { target_branch: 'master' })
+ visit project_new_merge_request_path(project, merge_request: { target_branch: 'master', source_branch: 'deleted-image-test' })
click_link "Changes"
@@ -165,8 +165,7 @@ describe 'Merge request > User selects branches for new MR', :js do
it 'shows pipelines for a new merge request' do
visit project_new_merge_request_path(
project,
- merge_request_source_branch: 'fix',
- merge_request: { target_branch: 'master' })
+ merge_request: { target_branch: 'master', source_branch: 'fix' })
page.within('.merge-request') do
click_link 'Pipelines'
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 6e681185e1f..b81478a481f 100644
--- a/spec/features/merge_request/user_uses_quick_actions_spec.rb
+++ b/spec/features/merge_request/user_uses_quick_actions_spec.rb
@@ -144,7 +144,7 @@ describe 'Merge request > User uses quick actions', :js do
describe '/target_branch command in merge request' do
let(:another_project) { create(:project, :public, :repository) }
- let(:new_url_opts) { { merge_request_source_branch: 'feature' } }
+ let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } }
before do
another_project.add_maintainer(user)
diff --git a/spec/features/merge_requests/user_squashes_merge_request_spec.rb b/spec/features/merge_requests/user_squashes_merge_request_spec.rb
index 8ecdec491b8..ec1153b7f7f 100644
--- a/spec/features/merge_requests/user_squashes_merge_request_spec.rb
+++ b/spec/features/merge_requests/user_squashes_merge_request_spec.rb
@@ -65,7 +65,7 @@ describe 'User squashes a merge request', :js do
context 'when squash is enabled on merge request creation' do
before do
- visit project_new_merge_request_path(project, merge_request_source_branch: source_branch, merge_request: { target_branch: 'master' })
+ visit project_new_merge_request_path(project, merge_request: { target_branch: 'master', source_branch: source_branch })
check 'merge_request[squash]'
click_on 'Submit merge request'
wait_for_requests
@@ -95,7 +95,7 @@ describe 'User squashes a merge request', :js do
context 'when squash is not enabled on merge request creation' do
before do
- visit project_new_merge_request_path(project, merge_request_source_branch: source_branch, merge_request: { target_branch: 'master' })
+ visit project_new_merge_request_path(project, merge_request: { target_branch: 'master', source_branch: source_branch })
click_on 'Submit merge request'
wait_for_requests
end
diff --git a/spec/features/milestones/user_creates_milestone_spec.rb b/spec/features/milestones/user_creates_milestone_spec.rb
index 8fd057d587c..5de0c381cdf 100644
--- a/spec/features/milestones/user_creates_milestone_spec.rb
+++ b/spec/features/milestones/user_creates_milestone_spec.rb
@@ -24,6 +24,6 @@ describe "User creates milestone", :js do
visit(activity_project_path(project))
- expect(page).to have_content("#{user.name} opened milestone")
+ expect(page).to have_content("#{user.name} #{user.to_reference} opened milestone")
end
end
diff --git a/spec/features/milestones/user_deletes_milestone_spec.rb b/spec/features/milestones/user_deletes_milestone_spec.rb
index a8c296b4cd2..f68ed1cde07 100644
--- a/spec/features/milestones/user_deletes_milestone_spec.rb
+++ b/spec/features/milestones/user_deletes_milestone_spec.rb
@@ -23,7 +23,7 @@ describe "User deletes milestone", :js do
visit(activity_project_path(project))
- expect(page).to have_content("#{user.name} destroyed milestone")
+ expect(page).to have_content("#{user.name} #{user.to_reference} destroyed milestone")
end
end
diff --git a/spec/features/projects/activity/user_sees_activity_spec.rb b/spec/features/projects/activity/user_sees_activity_spec.rb
index ebaa137772d..bb4b2abc3c7 100644
--- a/spec/features/projects/activity/user_sees_activity_spec.rb
+++ b/spec/features/projects/activity/user_sees_activity_spec.rb
@@ -19,13 +19,13 @@ describe 'Projects > Activity > User sees activity' do
it 'shows the last push in the activity page', :js do
visit activity_project_path(project)
- expect(page).to have_content "#{user.name} pushed new branch fix"
+ expect(page).to have_content "#{user.name} #{user.to_reference} pushed new branch fix"
end
it 'allows to filter event with the "event_filter=issue" URL param', :js do
visit activity_project_path(project, event_filter: 'issue')
- expect(page).not_to have_content "#{user.name} pushed new branch fix"
- expect(page).to have_content "#{user.name} opened issue #{issue.to_reference}"
+ expect(page).not_to have_content "#{user.name} #{user.to_reference} pushed new branch fix"
+ expect(page).to have_content "#{user.name} #{user.to_reference} opened issue #{issue.to_reference}"
end
end
diff --git a/spec/features/projects/activity/user_sees_private_activity_spec.rb b/spec/features/projects/activity/user_sees_private_activity_spec.rb
index d7dc0a6712a..61ec2ce9d29 100644
--- a/spec/features/projects/activity/user_sees_private_activity_spec.rb
+++ b/spec/features/projects/activity/user_sees_private_activity_spec.rb
@@ -5,7 +5,7 @@ describe 'Project > Activity > User sees private activity', :js do
let(:author) { create(:user) }
let(:user) { create(:user) }
let(:issue) { create(:issue, :confidential, project: project, author: author) }
- let(:message) { "#{author.name} opened issue #{issue.to_reference}" }
+ let(:message) { "#{author.name} #{author.to_reference} opened issue #{issue.to_reference}" }
before do
project.add_developer(author)
diff --git a/spec/features/projects/badges/pipeline_badge_spec.rb b/spec/features/projects/badges/pipeline_badge_spec.rb
index 8c4488b2ca6..dee81898928 100644
--- a/spec/features/projects/badges/pipeline_badge_spec.rb
+++ b/spec/features/projects/badges/pipeline_badge_spec.rb
@@ -19,7 +19,7 @@ describe 'Pipeline Badge' do
let!(:pipeline) { create(:ci_empty_pipeline, project: project, ref: ref, sha: project.commit(ref).sha) }
let!(:job) { create(:ci_build, pipeline: pipeline) }
- context 'when the pipeline was successfull' do
+ context 'when the pipeline was successful' do
it 'displays so on the badge' do
job.success
diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb
index 8b92b9fc869..3d17eb3a73a 100644
--- a/spec/features/projects/clusters/gcp_spec.rb
+++ b/spec/features/projects/clusters/gcp_spec.rb
@@ -130,6 +130,7 @@ describe 'Gcp Cluster', :js do
context 'when user changes cluster parameters' do
before do
+ allow(ClusterPlatformConfigureWorker).to receive(:perform_async)
fill_in 'cluster_platform_kubernetes_attributes_namespace', with: 'my-namespace'
page.within('#js-cluster-details') { click_button 'Save changes' }
end
diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb
index 9ae1dba60b5..250c964cc32 100644
--- a/spec/features/projects/clusters/user_spec.rb
+++ b/spec/features/projects/clusters/user_spec.rb
@@ -9,7 +9,9 @@ describe 'User Cluster', :js do
before do
project.add_maintainer(user)
gitlab_sign_in(user)
+
allow(Projects::ClustersController).to receive(:STATUS_POLLING_INTERVAL) { 100 }
+ allow_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute)
end
context 'when user does not have a cluster and visits cluster index page' do
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
index 91eac9c8278..a85e7333ba8 100644
--- a/spec/features/projects/clusters_spec.rb
+++ b/spec/features/projects/clusters_spec.rb
@@ -35,37 +35,6 @@ describe 'Clusters', :js do
expect(page).to have_selector('.gl-responsive-table-row', count: 2)
end
- context 'inline update of cluster' do
- it 'user can update cluster' do
- expect(page).to have_selector('.js-project-feature-toggle')
- end
-
- context 'with sucessfull request' do
- it 'user sees updated cluster' do
- expect do
- page.find('.js-project-feature-toggle').click
- wait_for_requests
- end.to change { cluster.reload.enabled }
-
- expect(page).not_to have_selector('.is-checked')
- expect(cluster.reload).not_to be_enabled
- end
- end
-
- context 'with failed request' do
- it 'user sees not update cluster and error message' do
- expect_any_instance_of(Clusters::UpdateService).to receive(:execute).and_call_original
- allow_any_instance_of(Clusters::Cluster).to receive(:valid?) { false }
-
- page.find('.js-project-feature-toggle').click
-
- expect(page).to have_content('Something went wrong on our end.')
- expect(page).to have_selector('.is-checked')
- expect(cluster.reload).to be_enabled
- end
- end
- end
-
context 'when user clicks on a cluster' do
before do
click_link cluster.name
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index 70e0879dd81..9772a7bacac 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -25,7 +25,7 @@ describe 'Environment' do
end
context 'without deployments' do
- it 'does show no deployments' do
+ it 'does not show deployments' do
expect(page).to have_content('You don\'t have any deployments right now.')
end
end
@@ -33,7 +33,7 @@ describe 'Environment' do
context 'with deployments' do
context 'when there is no related deployable' do
let(:deployment) do
- create(:deployment, environment: environment, deployable: nil)
+ create(:deployment, :success, environment: environment, deployable: nil)
end
it 'does show deployment SHA' do
@@ -43,20 +43,70 @@ describe 'Environment' do
end
end
+ context 'when there is a successful deployment' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, :success, pipeline: pipeline) }
+
+ let(:deployment) do
+ create(:deployment, :success, environment: environment, deployable: build)
+ end
+
+ it 'does show deployments' do
+ expect(page).to have_link("#{build.name} (##{build.id})")
+ end
+ end
+
+ context 'when there is a running deployment' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ let(:deployment) do
+ create(:deployment, :running, environment: environment, deployable: build)
+ end
+
+ it 'does not show deployments' do
+ expect(page).to have_content('You don\'t have any deployments right now.')
+ end
+ end
+
+ context 'when there is a failed deployment' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ let(:deployment) do
+ create(:deployment, :failed, environment: environment, deployable: build)
+ end
+
+ it 'does not show deployments' do
+ expect(page).to have_content('You don\'t have any deployments right now.')
+ end
+ end
+
context 'with related deployable present' do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) }
let(:deployment) do
- create(:deployment, environment: environment, deployable: build)
+ create(:deployment, :success, environment: environment, deployable: build)
end
it 'does show build name' do
expect(page).to have_link("#{build.name} (##{build.id})")
- expect(page).to have_link('Re-deploy')
+ expect(page).not_to have_link('Re-deploy')
expect(page).not_to have_terminal_button
end
+ context 'when user has ability to re-deploy' do
+ let(:permissions) do
+ create(:protected_branch, :developers_can_merge,
+ name: build.ref, project: project)
+ end
+
+ it 'does show re-deploy' do
+ expect(page).to have_link('Re-deploy')
+ end
+ end
+
context 'with manual action' do
let(:action) do
create(:ci_build, :manual, pipeline: pipeline,
@@ -97,7 +147,7 @@ describe 'Environment' do
context 'with external_url' do
let(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
let(:build) { create(:ci_build, pipeline: pipeline) }
- let(:deployment) { create(:deployment, environment: environment, deployable: build) }
+ let(:deployment) { create(:deployment, :success, environment: environment, deployable: build) }
it 'does show an external link button' do
expect(page).to have_link(nil, href: environment.external_url)
@@ -158,7 +208,8 @@ describe 'Environment' do
end
let(:deployment) do
- create(:deployment, environment: environment,
+ create(:deployment, :success,
+ environment: environment,
deployable: build,
on_stop: 'close_app')
end
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index 917ba495f01..89954d35f91 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -128,11 +128,12 @@ describe 'Environments page', :js do
end
end
- context 'when there are deployments' do
+ context 'when there are successful deployments' do
let(:project) { create(:project, :repository) }
let!(:deployment) do
- create(:deployment, environment: environment,
+ create(:deployment, :success,
+ environment: environment,
sha: project.commit.id)
end
@@ -152,7 +153,8 @@ describe 'Environments page', :js do
end
let!(:deployment) do
- create(:deployment, environment: environment,
+ create(:deployment, :success,
+ environment: environment,
deployable: build,
sha: project.commit.id)
end
@@ -162,7 +164,7 @@ describe 'Environments page', :js do
end
it 'shows a play button' do
- find('.js-dropdown-play-icon-container').click
+ find('.js-environment-actions-dropdown').click
expect(page).to have_content(action.name.humanize)
end
@@ -170,7 +172,7 @@ describe 'Environments page', :js do
it 'allows to play a manual action', :js do
expect(action).to be_manual
- find('.js-dropdown-play-icon-container').click
+ find('.js-environment-actions-dropdown').click
expect(page).to have_content(action.name.humanize)
expect { find('.js-manual-action-link').click }
@@ -196,7 +198,7 @@ describe 'Environments page', :js do
context 'with external_url' do
let(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
let(:build) { create(:ci_build, pipeline: pipeline) }
- let(:deployment) { create(:deployment, environment: environment, deployable: build) }
+ let(:deployment) { create(:deployment, :success, environment: environment, deployable: build) }
it 'shows an external link button' do
expect(page).to have_link(nil, href: environment.external_url)
@@ -209,7 +211,8 @@ describe 'Environments page', :js do
end
let(:deployment) do
- create(:deployment, environment: environment,
+ create(:deployment, :success,
+ environment: environment,
deployable: build,
on_stop: 'close_app')
end
@@ -260,6 +263,86 @@ describe 'Environments page', :js do
end
end
end
+
+ context 'when there is a delayed job' do
+ let!(:pipeline) { create(:ci_pipeline, project: project) }
+ let!(:build) { create(:ci_build, pipeline: pipeline) }
+
+ let!(:delayed_job) do
+ create(:ci_build, :scheduled,
+ pipeline: pipeline,
+ name: 'delayed job',
+ stage: 'test',
+ commands: 'test')
+ end
+
+ let!(:deployment) do
+ create(:deployment,
+ :success,
+ environment: environment,
+ deployable: build,
+ sha: project.commit.id)
+ end
+
+ before do
+ visit_environments(project)
+ end
+
+ it 'has a dropdown for actionable jobs' do
+ expect(page).to have_selector('.dropdown-new.btn.btn-default .ic-play')
+ end
+
+ it "has link to the delayed job's action" do
+ find('.js-environment-actions-dropdown').click
+
+ expect(page).to have_button('Delayed job')
+ expect(page).to have_content(/\d{2}:\d{2}:\d{2}/)
+ end
+
+ context 'when delayed job is expired already' do
+ let!(:delayed_job) do
+ create(:ci_build, :expired_scheduled,
+ pipeline: pipeline,
+ name: 'delayed job',
+ stage: 'test',
+ commands: 'test')
+ end
+
+ it "shows 00:00:00 as the remaining time" do
+ find('.js-environment-actions-dropdown').click
+
+ expect(page).to have_content("00:00:00")
+ end
+ end
+
+ context 'when user played a delayed job immediately' do
+ before do
+ find('.js-environment-actions-dropdown').click
+ page.accept_confirm { click_button('Delayed job') }
+ wait_for_requests
+ end
+
+ it 'enqueues the delayed job', :js do
+ expect(delayed_job.reload).to be_pending
+ end
+ end
+ end
+ end
+
+ context 'when there is a failed deployment' do
+ let(:project) { create(:project, :repository) }
+
+ let!(:deployment) do
+ create(:deployment, :failed,
+ environment: environment,
+ sha: project.commit.id)
+ end
+
+ it 'does not show deployments' do
+ visit_environments(project)
+
+ expect(page).to have_content('No deployments yet')
+ end
end
end
diff --git a/spec/features/projects/files/user_creates_directory_spec.rb b/spec/features/projects/files/user_creates_directory_spec.rb
index 6f620dff82b..847b5f0860f 100644
--- a/spec/features/projects/files/user_creates_directory_spec.rb
+++ b/spec/features/projects/files/user_creates_directory_spec.rb
@@ -57,7 +57,7 @@ describe 'Projects > Files > User creates a directory', :js do
expect(page).to have_content('From new-feature into master')
expect(page).to have_content('Add new directory')
- expect(current_path).to eq(project_new_merge_request_path(project, merge_request_source_branch: "new-feature"))
+ expect(current_path).to eq(project_new_merge_request_path(project))
end
end
@@ -80,7 +80,8 @@ describe 'Projects > Files > User creates a directory', :js do
click_button('Create directory')
fork = user.fork_of(project2.reload)
- expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "patch-1"))
+
+ expect(current_path).to eq(project_new_merge_request_path(fork))
end
end
end
diff --git a/spec/features/projects/files/user_creates_files_spec.rb b/spec/features/projects/files/user_creates_files_spec.rb
index d9df4b50621..a4f94b7a76d 100644
--- a/spec/features/projects/files/user_creates_files_spec.rb
+++ b/spec/features/projects/files/user_creates_files_spec.rb
@@ -16,7 +16,7 @@ describe 'Projects > Files > User creates files' do
sign_in(user)
end
- context 'without commiting a new file' do
+ context 'without committing a new file' do
context 'when an user has write access' do
before do
visit(project_tree_path_root_ref)
@@ -49,7 +49,7 @@ describe 'Projects > Files > User creates files' do
end
end
- context 'with commiting a new file' do
+ context 'with committing a new file' do
context 'when an user has write access' do
before do
visit(project_tree_path_root_ref)
@@ -144,7 +144,7 @@ describe 'Projects > Files > User creates files' do
fill_in(:branch_name, with: 'new_branch_name', visible: true)
click_button('Commit changes')
- expect(current_path).to eq(project_new_merge_request_path(project, merge_request_source_branch: "new_branch_name"))
+ expect(current_path).to eq(project_new_merge_request_path(project))
click_link('Changes')
@@ -182,7 +182,7 @@ describe 'Projects > Files > User creates files' do
fork = user.fork_of(project2.reload)
- expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "patch-1"))
+ expect(current_path).to eq(project_new_merge_request_path(fork))
expect(page).to have_content('New commit message')
end
end
diff --git a/spec/features/projects/files/user_deletes_files_spec.rb b/spec/features/projects/files/user_deletes_files_spec.rb
index faf11ee9dd8..614b11fa5c8 100644
--- a/spec/features/projects/files/user_deletes_files_spec.rb
+++ b/spec/features/projects/files/user_deletes_files_spec.rb
@@ -63,7 +63,7 @@ describe 'Projects > Files > User deletes files', :js do
fork = user.fork_of(project2.reload)
- expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "patch-1"))
+ expect(current_path).to eq(project_new_merge_request_path(fork))
expect(page).to have_content('New commit message')
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 c6b2aaea906..9eb65ec159c 100644
--- a/spec/features/projects/files/user_edits_files_spec.rb
+++ b/spec/features/projects/files/user_edits_files_spec.rb
@@ -86,7 +86,7 @@ describe 'Projects > Files > User edits files', :js do
fill_in(:branch_name, with: 'new_branch_name', visible: true)
click_button('Commit changes')
- expect(current_path).to eq(project_new_merge_request_path(project, merge_request_source_branch: "new_branch_name"))
+ expect(current_path).to eq(project_new_merge_request_path(project))
click_link('Changes')
@@ -155,7 +155,7 @@ describe 'Projects > Files > User edits files', :js do
fork = user.fork_of(project2.reload)
- expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "patch-1"))
+ expect(current_path).to eq(project_new_merge_request_path(fork))
wait_for_requests
@@ -183,7 +183,7 @@ describe 'Projects > Files > User edits files', :js do
fork = user.fork_of(project2)
- expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "patch-1"))
+ expect(current_path).to eq(project_new_merge_request_path(fork))
wait_for_requests
diff --git a/spec/features/projects/files/user_replaces_files_spec.rb b/spec/features/projects/files/user_replaces_files_spec.rb
index 09feb315465..e3da28d73c3 100644
--- a/spec/features/projects/files/user_replaces_files_spec.rb
+++ b/spec/features/projects/files/user_replaces_files_spec.rb
@@ -78,7 +78,7 @@ describe 'Projects > Files > User replaces files', :js do
fork = user.fork_of(project2.reload)
- expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "undefined"))
+ expect(current_path).to eq(project_new_merge_request_path(fork))
click_link('Changes')
diff --git a/spec/features/projects/files/user_uploads_files_spec.rb b/spec/features/projects/files/user_uploads_files_spec.rb
index 92df8303f46..af3fc528a20 100644
--- a/spec/features/projects/files/user_uploads_files_spec.rb
+++ b/spec/features/projects/files/user_uploads_files_spec.rb
@@ -36,7 +36,7 @@ describe 'Projects > Files > User uploads files' do
click_button('Upload file')
expect(page).to have_content('New commit message')
- expect(current_path).to eq(project_new_merge_request_path(project, merge_request_source_branch: "new_branch_name"))
+ expect(current_path).to eq(project_new_merge_request_path(project))
click_link('Changes')
find("a[data-action='diffs']", text: 'Changes').click
@@ -92,7 +92,7 @@ describe 'Projects > Files > User uploads files' do
fork = user.fork_of(project2.reload)
- expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "undefined"))
+ expect(current_path).to eq(project_new_merge_request_path(fork))
find("a[data-action='diffs']", text: 'Changes').click
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index b3bea92e635..a1323699969 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -198,6 +198,24 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
end
end
+ context 'when job is running', :js do
+ let(:job) { create(:ci_build, :running, pipeline: pipeline) }
+ let(:job_url) { project_job_path(project, job) }
+
+ before do
+ visit job_url
+ wait_for_requests
+ end
+
+ context 'job is cancelable' do
+ it 'shows cancel button' do
+ click_link 'Cancel'
+
+ expect(page.current_path).to eq(job_url)
+ end
+ end
+ end
+
context "Job from other project" do
before do
visit project_job_path(project, job2)
@@ -378,8 +396,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
end
context 'job is successful and has deployment' do
- let(:build) { create(:ci_build, :success, :trace_live, environment: environment.name, pipeline: pipeline) }
- let!(:deployment) { create(:deployment, environment: environment, project: environment.project, deployable: build) }
+ let(:build) { create(:ci_build, :success, :trace_live, environment: environment.name, pipeline: pipeline, deployment: deployment) }
+ let(:deployment) { create(:deployment, :success, environment: environment, project: environment.project) }
it 'shows a link for the job' do
expect(page).to have_link environment.name
@@ -401,7 +419,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
end
context 'deployment still not finished' do
- let(:build) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) }
+ let(:build) { create(:ci_build, :running, environment: environment.name, pipeline: pipeline) }
it 'shows a link to latest deployment' do
expect(page).to have_link environment.name
@@ -438,6 +456,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
describe 'environment info in job view', :js do
before do
+ allow_any_instance_of(Ci::Build).to receive(:create_deployment)
+
visit project_job_path(project, job)
wait_for_requests
end
@@ -446,8 +466,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
let(:job) { create(:ci_build, :success, :trace_artifact, environment: 'staging', pipeline: pipeline) }
let(:second_build) { create(:ci_build, :success, :trace_artifact, environment: 'staging', pipeline: pipeline) }
let(:environment) { create(:environment, name: 'staging', project: project) }
- let!(:first_deployment) { create(:deployment, environment: environment, deployable: job) }
- let!(:second_deployment) { create(:deployment, environment: environment, deployable: second_build) }
+ let!(:first_deployment) { create(:deployment, :success, environment: environment, deployable: job) }
+ let!(:second_deployment) { create(:deployment, :success, environment: environment, deployable: second_build) }
it 'shows deployment message' do
expected_text = 'This job is an out-of-date deployment ' \
@@ -487,7 +507,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
end
context 'when it has deployment' do
- let!(:deployment) { create(:deployment, environment: environment) }
+ let!(:deployment) { create(:deployment, :success, environment: environment) }
it 'shows that deployment will be overwritten' do
expected_text = 'This job is creating a deployment to staging'
@@ -575,7 +595,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
end
it 'shows delayed job', :js do
- expect(page).to have_content('This is a delayed to run in')
+ expect(page).to have_content('This is a delayed job to run in')
expect(page).to have_content("This job will automatically run after it's timer finishes.")
expect(page).to have_link('Unschedule job')
end
diff --git a/spec/features/projects/merge_request_button_spec.rb b/spec/features/projects/merge_request_button_spec.rb
index 4be31511ceb..69561b4d733 100644
--- a/spec/features/projects/merge_request_button_spec.rb
+++ b/spec/features/projects/merge_request_button_spec.rb
@@ -22,8 +22,8 @@ describe 'Merge Request button' do
it 'shows Create merge request button' do
href = project_new_merge_request_path(project,
- merge_request_source_branch: 'feature',
- merge_request: { target_branch: 'master' })
+ merge_request: { source_branch: 'feature',
+ target_branch: 'master' })
visit url
@@ -77,8 +77,8 @@ describe 'Merge Request button' do
it 'shows Create merge request button' do
href = project_new_merge_request_path(forked_project,
- merge_request_source_branch: 'feature',
- merge_request: { target_branch: 'master' })
+ merge_request: { source_branch: 'feature',
+ target_branch: 'master' })
visit fork_url
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index cd6c37bf54d..049bbca958f 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -388,54 +388,83 @@ describe 'Pipeline', :js do
let(:pipeline_failures_page) { failures_project_pipeline_path(project, pipeline) }
let!(:failed_build) { create(:ci_build, :failed, pipeline: pipeline) }
+ subject { visit pipeline_failures_page }
+
context 'with failed build' do
before do
failed_build.trace.set('4 examples, 1 failure')
-
- visit pipeline_failures_page
end
it 'shows jobs tab pane as active' do
+ subject
+
expect(page).to have_content('Failed Jobs')
expect(page).to have_css('#js-tab-failures.active')
end
it 'lists failed builds' do
+ subject
+
expect(page).to have_content(failed_build.name)
expect(page).to have_content(failed_build.stage)
end
it 'shows build failure logs' do
+ subject
+
expect(page).to have_content('4 examples, 1 failure')
end
it 'shows the failure reason' do
+ subject
+
expect(page).to have_content('There is an unknown failure, please try again')
end
- it 'shows retry button for failed build' do
- page.within(find('.build-failures', match: :first)) do
- expect(page).to have_link('Retry')
+ context 'when user does not have permission to retry build' do
+ it 'shows retry button for failed build' do
+ subject
+
+ page.within(find('.build-failures', match: :first)) do
+ expect(page).not_to have_link('Retry')
+ end
end
end
- end
- context 'when missing build logs' do
- before do
- visit pipeline_failures_page
+ context 'when user does have permission to retry build' do
+ before do
+ create(:protected_branch, :developers_can_merge,
+ name: pipeline.ref, project: project)
+ end
+
+ it 'shows retry button for failed build' do
+ subject
+
+ page.within(find('.build-failures', match: :first)) do
+ expect(page).to have_link('Retry')
+ end
+ end
end
+ end
+ context 'when missing build logs' do
it 'shows jobs tab pane as active' do
+ subject
+
expect(page).to have_content('Failed Jobs')
expect(page).to have_css('#js-tab-failures.active')
end
it 'lists failed builds' do
+ subject
+
expect(page).to have_content(failed_build.name)
expect(page).to have_content(failed_build.stage)
end
it 'does not show trace' do
+ subject
+
expect(page).to have_content('No job trace')
end
end
@@ -448,11 +477,9 @@ describe 'Pipeline', :js do
end
context 'when accessing failed jobs page' do
- before do
- visit pipeline_failures_page
- end
-
it 'fails to access the page' do
+ subject
+
expect(page).to have_title('Access Denied')
end
end
@@ -461,11 +488,11 @@ describe 'Pipeline', :js do
context 'without failures' do
before do
failed_build.update!(status: :success)
-
- visit pipeline_failures_page
end
it 'displays the pipeline graph' do
+ subject
+
expect(current_path).to eq(pipeline_path(pipeline))
expect(page).not_to have_content('Failed Jobs')
expect(page).to have_selector('.pipeline-visualization')
diff --git a/spec/features/projects/view_on_env_spec.rb b/spec/features/projects/view_on_env_spec.rb
index a48ad94e9fa..7bfcd46713e 100644
--- a/spec/features/projects/view_on_env_spec.rb
+++ b/spec/features/projects/view_on_env_spec.rb
@@ -44,7 +44,7 @@ describe 'View on environment', :js do
context 'and an active deployment' do
let(:sha) { project.commit(branch_name).sha }
let(:environment) { create(:environment, project: project, name: 'review/feature', external_url: 'http://feature.review.example.com') }
- let!(:deployment) { create(:deployment, environment: environment, ref: branch_name, sha: sha) }
+ let!(:deployment) { create(:deployment, :success, environment: environment, ref: branch_name, sha: sha) }
context 'when visiting the diff of a merge request for the branch' do
let(:merge_request) { create(:merge_request, :simple, source_project: project, source_branch: branch_name) }
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 3ee753b7d23..7225ca65492 100644
--- a/spec/features/search/user_searches_for_wiki_pages_spec.rb
+++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe 'User searches for wiki pages', :js do
let(:user) { create(:user) }
- let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
+ let(:project) { create(:project, :repository, :wiki_repo, namespace: user.namespace) }
let!(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'test_wiki', content: 'Some Wiki content' }) }
before do
diff --git a/spec/features/user_sorts_things_spec.rb b/spec/features/user_sorts_things_spec.rb
index 69ebdddaeec..0295f588326 100644
--- a/spec/features/user_sorts_things_spec.rb
+++ b/spec/features/user_sorts_things_spec.rb
@@ -6,7 +6,7 @@ require "spec_helper"
# All those specs are moved out to this spec intentionally to keep them all in one place.
describe "User sorts things" do
include Spec::Support::Helpers::Features::SortingHelpers
- include Helpers::DashboardHelper
+ include DashboardHelper
set(:project) { create(:project_empty_repo, :public) }
set(:current_user) { create(:user) } # Using `current_user` instead of just `user` because of the hardoced call in `assigned_mrs_dashboard_path` which is used below.
diff --git a/spec/finders/environments_finder_spec.rb b/spec/finders/environments_finder_spec.rb
index 3cd421f22eb..25835bb4d94 100644
--- a/spec/finders/environments_finder_spec.rb
+++ b/spec/finders/environments_finder_spec.rb
@@ -12,7 +12,7 @@ describe EnvironmentsFinder do
context 'tagged deployment' do
before do
- create(:deployment, environment: environment, ref: 'v1.1.0', tag: true, sha: project.commit.id)
+ create(:deployment, :success, environment: environment, ref: 'v1.1.0', tag: true, sha: project.commit.id)
end
it 'returns environment when with_tags is set' do
@@ -33,7 +33,7 @@ describe EnvironmentsFinder do
context 'branch deployment' do
before do
- create(:deployment, environment: environment, ref: 'master', sha: project.commit.id)
+ create(:deployment, :success, environment: environment, ref: 'master', sha: project.commit.id)
end
it 'returns environment when ref is set' do
@@ -59,7 +59,7 @@ describe EnvironmentsFinder do
context 'commit deployment' do
before do
- create(:deployment, environment: environment, ref: 'master', sha: project.commit.id)
+ create(:deployment, :success, environment: environment, ref: 'master', sha: project.commit.id)
end
it 'returns environment' do
@@ -71,7 +71,7 @@ describe EnvironmentsFinder do
context 'recently updated' do
context 'when last deployment to environment is the most recent one' do
before do
- create(:deployment, environment: environment, ref: 'feature')
+ create(:deployment, :success, environment: environment, ref: 'feature')
end
it 'finds recently updated environment' do
@@ -82,8 +82,8 @@ describe EnvironmentsFinder do
context 'when last deployment to environment is not the most recent' do
before do
- create(:deployment, environment: environment, ref: 'feature')
- create(:deployment, environment: environment, ref: 'master')
+ create(:deployment, :success, environment: environment, ref: 'feature')
+ create(:deployment, :success, environment: environment, ref: 'master')
end
it 'does not find environment' do
@@ -96,8 +96,8 @@ describe EnvironmentsFinder do
let(:second_environment) { create(:environment, project: project) }
before do
- create(:deployment, environment: environment, ref: 'feature')
- create(:deployment, environment: second_environment, ref: 'feature')
+ create(:deployment, :success, environment: environment, ref: 'feature')
+ create(:deployment, :success, environment: second_environment, ref: 'feature')
end
it 'finds both environments' do
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 2f164ffa8b0..c0488c83bd8 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -144,19 +144,31 @@ describe IssuesFinder do
end
context 'filtering by no milestone' do
- let(:params) { { milestone_title: Milestone::None.title } }
+ let(:params) { { milestone_title: 'None' } }
it 'returns issues with no milestone' do
expect(issues).to contain_exactly(issue2, issue3, issue4)
end
+
+ it 'returns issues with no milestone (deprecated)' do
+ params[:milestone_title] = Milestone::None.title
+
+ expect(issues).to contain_exactly(issue2, issue3, issue4)
+ end
end
context 'filtering by any milestone' do
- let(:params) { { milestone_title: Milestone::Any.title } }
+ let(:params) { { milestone_title: 'Any' } }
it 'returns issues with any assigned milestone' do
expect(issues).to contain_exactly(issue1)
end
+
+ it 'returns issues with any assigned milestone (deprecated)' do
+ params[:milestone_title] = Milestone::Any.title
+
+ expect(issues).to contain_exactly(issue1)
+ end
end
context 'filtering by upcoming milestone' do
@@ -360,6 +372,22 @@ describe IssuesFinder do
end
context 'filtering by reaction name' do
+ context 'user searches by no reaction' do
+ let(:params) { { my_reaction_emoji: 'None' } }
+
+ it 'returns issues that the user did not react to' do
+ expect(issues).to contain_exactly(issue2, issue4)
+ end
+ end
+
+ context 'user searches by any reaction' do
+ let(:params) { { my_reaction_emoji: 'Any' } }
+
+ it 'returns issues that the user reacted to' do
+ expect(issues).to contain_exactly(issue1, issue3)
+ end
+ end
+
context 'user searches by "thumbsup" reaction' do
let(:params) { { my_reaction_emoji: 'thumbsup' } }
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index de9974c45e1..b51f1955ac4 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -13,7 +13,7 @@ describe NotesFinder do
let!(:comment) { create(:note_on_issue, project: project) }
let!(:system_note) { create(:note_on_issue, project: project, system: true) }
- it 'filters system notes' do
+ it 'returns only user notes when using only_comments filter' do
finder = described_class.new(project, user, notes_filter: UserPreference::NOTES_FILTERS[:only_comments])
notes = finder.execute
@@ -21,6 +21,14 @@ describe NotesFinder do
expect(notes).to match_array(comment)
end
+ it 'returns only system notes when using only_activity filters' do
+ finder = described_class.new(project, user, notes_filter: UserPreference::NOTES_FILTERS[:only_activity])
+
+ notes = finder.execute
+
+ expect(notes).to match_array(system_note)
+ end
+
it 'gets all notes' do
finder = described_class.new(project, user, notes_filter: UserPreference::NOTES_FILTERS[:all_activity])
diff --git a/spec/finders/personal_access_tokens_finder_spec.rb b/spec/finders/personal_access_tokens_finder_spec.rb
index 3f22b3a253d..3e849c9a644 100644
--- a/spec/finders/personal_access_tokens_finder_spec.rb
+++ b/spec/finders/personal_access_tokens_finder_spec.rb
@@ -92,7 +92,7 @@ describe PersonalAccessTokensFinder do
end
describe 'with id' do
- subject { finder(params).find_by(id: active_personal_access_token.id) }
+ subject { finder(params).find_by_id(active_personal_access_token.id) }
it { is_expected.to eq(active_personal_access_token) }
@@ -106,7 +106,7 @@ describe PersonalAccessTokensFinder do
end
describe 'with token' do
- subject { finder(params).find_by(token: active_personal_access_token.token) }
+ subject { finder(params).find_by_token(active_personal_access_token.token) }
it { is_expected.to eq(active_personal_access_token) }
@@ -207,7 +207,7 @@ describe PersonalAccessTokensFinder do
end
describe 'with id' do
- subject { finder(params).find_by(id: active_personal_access_token.id) }
+ subject { finder(params).find_by_id(active_personal_access_token.id) }
it { is_expected.to eq(active_personal_access_token) }
@@ -221,7 +221,7 @@ describe PersonalAccessTokensFinder do
end
describe 'with token' do
- subject { finder(params).find_by(token: active_personal_access_token.token) }
+ subject { finder(params).find_by_token(active_personal_access_token.token) }
it { is_expected.to eq(active_personal_access_token) }
diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb
index 1ae0bd988f2..dfeeb3040c6 100644
--- a/spec/finders/snippets_finder_spec.rb
+++ b/spec/finders/snippets_finder_spec.rb
@@ -4,16 +4,13 @@ describe SnippetsFinder do
include Gitlab::Allowable
using RSpec::Parameterized::TableSyntax
- context 'filter by visibility' do
- let!(:snippet1) { create(:personal_snippet, :private) }
- let!(:snippet2) { create(:personal_snippet, :internal) }
- let!(:snippet3) { create(:personal_snippet, :public) }
+ describe '#initialize' do
+ it 'raises ArgumentError when a project and author are given' do
+ user = build(:user)
+ project = build(:project)
- it "returns public snippets when visibility is PUBLIC" do
- snippets = described_class.new(nil, visibility: Snippet::PUBLIC).execute
-
- expect(snippets).to include(snippet3)
- expect(snippets).not_to include(snippet1, snippet2)
+ expect { described_class.new(user, author: user, project: project) }
+ .to raise_error(ArgumentError)
end
end
@@ -66,21 +63,21 @@ describe SnippetsFinder do
end
it "returns internal snippets" do
- snippets = described_class.new(user, author: user, visibility: Snippet::INTERNAL).execute
+ snippets = described_class.new(user, author: user, scope: :are_internal).execute
expect(snippets).to include(snippet2)
expect(snippets).not_to include(snippet1, snippet3)
end
it "returns private snippets" do
- snippets = described_class.new(user, author: user, visibility: Snippet::PRIVATE).execute
+ snippets = described_class.new(user, author: user, scope: :are_private).execute
expect(snippets).to include(snippet1)
expect(snippets).not_to include(snippet2, snippet3)
end
it "returns public snippets" do
- snippets = described_class.new(user, author: user, visibility: Snippet::PUBLIC).execute
+ snippets = described_class.new(user, author: user, scope: :are_public).execute
expect(snippets).to include(snippet3)
expect(snippets).not_to include(snippet1, snippet2)
@@ -98,6 +95,13 @@ describe SnippetsFinder do
expect(snippets).to include(snippet3)
expect(snippets).not_to include(snippet2, snippet1)
end
+
+ it 'returns all snippets for an admin' do
+ admin = create(:user, :admin)
+ snippets = described_class.new(admin, author: user).execute
+
+ expect(snippets).to include(snippet1, snippet2, snippet3)
+ end
end
context 'filter by project' do
@@ -126,21 +130,21 @@ describe SnippetsFinder do
end
it "returns public snippets for non project members" do
- snippets = described_class.new(user, project: project1, visibility: Snippet::PUBLIC).execute
+ snippets = described_class.new(user, project: project1, scope: :are_public).execute
expect(snippets).to include(@snippet3)
expect(snippets).not_to include(@snippet1, @snippet2)
end
it "returns internal snippets for non project members" do
- snippets = described_class.new(user, project: project1, visibility: Snippet::INTERNAL).execute
+ snippets = described_class.new(user, project: project1, scope: :are_internal).execute
expect(snippets).to include(@snippet2)
expect(snippets).not_to include(@snippet1, @snippet3)
end
it "does not return private snippets for non project members" do
- snippets = described_class.new(user, project: project1, visibility: Snippet::PRIVATE).execute
+ snippets = described_class.new(user, project: project1, scope: :are_private).execute
expect(snippets).not_to include(@snippet1, @snippet2, @snippet3)
end
@@ -156,10 +160,17 @@ describe SnippetsFinder do
it "returns private snippets for project members" do
project1.add_developer(user)
- snippets = described_class.new(user, project: project1, visibility: Snippet::PRIVATE).execute
+ snippets = described_class.new(user, project: project1, scope: :are_private).execute
expect(snippets).to include(@snippet1)
end
+
+ it 'returns all snippets for an admin' do
+ admin = create(:user, :admin)
+ snippets = described_class.new(admin, project: project1).execute
+
+ expect(snippets).to include(@snippet1, @snippet2, @snippet3)
+ end
end
describe '#execute' do
@@ -184,4 +195,6 @@ describe SnippetsFinder do
end
end
end
+
+ it_behaves_like 'snippet visibility'
end
diff --git a/spec/fixtures/api/schemas/deployment.json b/spec/fixtures/api/schemas/deployment.json
index 44835386cfc..0828f113495 100644
--- a/spec/fixtures/api/schemas/deployment.json
+++ b/spec/fixtures/api/schemas/deployment.json
@@ -48,6 +48,10 @@
"manual_actions": {
"type": "array",
"items": { "$ref": "job/job.json" }
+ },
+ "scheduled_actions": {
+ "type": "array",
+ "items": { "$ref": "job/job.json" }
}
},
"additionalProperties": false
diff --git a/spec/fixtures/api/schemas/entities/issue_board.json b/spec/fixtures/api/schemas/entities/issue_board.json
new file mode 100644
index 00000000000..3e252ddd13c
--- /dev/null
+++ b/spec/fixtures/api/schemas/entities/issue_board.json
@@ -0,0 +1,39 @@
+{
+ "type": "object",
+ "properties" : {
+ "id": { "type": "integer" },
+ "iid": { "type": "integer" },
+ "title": { "type": "string" },
+ "confidential": { "type": "boolean" },
+ "due_date": { "type": "date" },
+ "project_id": { "type": "integer" },
+ "relative_position": { "type": ["integer", "null"] },
+ "time_estimate": { "type": "integer" },
+ "weight": { "type": "integer" },
+ "project": {
+ "type": "object",
+ "properties": {
+ "id": { "type": "integer" },
+ "path": { "type": "string" }
+ }
+ },
+ "milestone": {
+ "type": "object",
+ "properties": {
+ "id": { "type": "integer" },
+ "title": { "type": "string" }
+ }
+ },
+ "assignees": { "type": ["array", "null"] },
+ "labels": {
+ "type": "array",
+ "items": { "$ref": "label.json" }
+ },
+ "reference_path": { "type": "string" },
+ "real_path": { "type": "string" },
+ "issue_sidebar_endpoint": { "type": "string" },
+ "toggle_subscription_endpoint": { "type": "string" },
+ "assignable_labels_endpoint": { "type": "string" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
index 8833825e3fb..a83ec55cede 100644
--- a/spec/fixtures/api/schemas/issue.json
+++ b/spec/fixtures/api/schemas/issue.json
@@ -13,8 +13,10 @@
"confidential": { "type": "boolean" },
"due_date": { "type": ["date", "null"] },
"relative_position": { "type": "integer" },
+ "time_estimate": { "type": "integer" },
"issue_sidebar_endpoint": { "type": "string" },
"toggle_subscription_endpoint": { "type": "string" },
+ "assignable_labels_endpoint": { "type": "string" },
"reference_path": { "type": "string" },
"real_path": { "type": "string" },
"project": {
diff --git a/spec/fixtures/api/schemas/job/job.json b/spec/fixtures/api/schemas/job/job.json
index 734c535ef70..f3d5e9b038a 100644
--- a/spec/fixtures/api/schemas/job/job.json
+++ b/spec/fixtures/api/schemas/job/job.json
@@ -9,7 +9,8 @@
"playable",
"created_at",
"updated_at",
- "status"
+ "status",
+ "archived"
],
"properties": {
"id": { "type": "integer" },
@@ -27,7 +28,8 @@
"updated_at": { "type": "string" },
"status": { "$ref": "../status/ci_detailed_status.json" },
"callout_message": { "type": "string" },
- "recoverable": { "type": "boolean" }
+ "recoverable": { "type": "boolean" },
+ "archived": { "type": "boolean" }
},
"additionalProperties": true
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
index f7adc4e0b91..6df27bf32b9 100644
--- a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
+++ b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
@@ -9,6 +9,32 @@
"title": { "type": "string" },
"description": { "type": ["string", "null"] },
"state": { "type": "string" },
+ "merged_by": {
+ "type": ["object", "null"],
+ "properties": {
+ "name": { "type": "string" },
+ "username": { "type": "string" },
+ "id": { "type": "integer" },
+ "state": { "type": "string" },
+ "avatar_url": { "type": "uri" },
+ "web_url": { "type": "uri" }
+ },
+ "additionalProperties": false
+ },
+ "merged_at": { "type": ["date", "null"] },
+ "closed_by": {
+ "type": ["object", "null"],
+ "properties": {
+ "name": { "type": "string" },
+ "username": { "type": "string" },
+ "id": { "type": "integer" },
+ "state": { "type": "string" },
+ "avatar_url": { "type": "uri" },
+ "web_url": { "type": "uri" }
+ },
+ "additionalProperties": false
+ },
+ "closed_at": { "type": ["date", "null"] },
"created_at": { "type": "date" },
"updated_at": { "type": "date" },
"target_branch": { "type": "string" },
diff --git a/spec/fixtures/emails/merge_request_multiple_patches.eml b/spec/fixtures/emails/merge_request_multiple_patches.eml
new file mode 100644
index 00000000000..311b99a525d
--- /dev/null
+++ b/spec/fixtures/emails/merge_request_multiple_patches.eml
@@ -0,0 +1,181 @@
+From: "Jake the Dog" <jake@adventuretime.ooo>
+To: incoming+gitlabhq/gitlabhq+merge-request+auth_token@appmail.adventuretime.ooo
+Subject: new-branch-with-a-patch
+Date: Wed, 31 Oct 2018 17:27:52 +0100
+X-Mailer: MailMate (1.12r5523)
+Message-ID: <7BE7C2E6-F0D9-4C85-9F55-B2A4BA01BFEC@gitlab.com>
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_="
+
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+
+This applies nicely to a branch freshly created from the root-ref
+
+The other attachments in this email are ignored
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+Content-Disposition: attachment;
+ filename=0002-This-does-not-apply-to-the-feature-branch.patch
+Content-Transfer-Encoding: quoted-printable
+
+=46rom 00c68c2b4f954370ce82a1162bc29c13f524897e Mon Sep 17 00:00:00 2001
+From: Patch user <patchuser@gitlab.org>
+Date: Mon, 22 Oct 2018 11:05:48 +0200
+Subject: [PATCH] This does not apply to the `feature` branch
+
+---
+ files/ruby/feature.rb | 5 +++++
+ 1 file changed, 5 insertions(+)
+ create mode 100644 files/ruby/feature.rb
+
+diff --git a/files/ruby/feature.rb b/files/ruby/feature.rb
+new file mode 100644
+index 0000000..fef26e4
+--- /dev/null
++++ b/files/ruby/feature.rb
+@@ -0,0 +1,5 @@
++class Feature
++ def bar
++ puts 'foo'
++ end
++end
+-- =
+
+2.19.1
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+Content-Disposition: attachment; filename=0001-A-commit-from-a-patch.patch
+Content-Transfer-Encoding: quoted-printable
+
+=46rom 3fee0042e610fb3563e4379e316704cb1210f3de Mon Sep 17 00:00:00 2001
+From: Patch user <patchuser@gitlab.org>
+Date: Thu, 18 Oct 2018 13:40:35 +0200
+Subject: [PATCH] A commit from a patch
+
+---
+ README | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/README b/README
+index 3742e48..e40a3b9 100644
+--- a/README
++++ b/README
+@@ -1 +1,3 @@
+ Sample repo for testing gitlab features
++
++This was applied in a patch!
+-- =
+
+2.19.1
+
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+Content-Disposition: attachment; filename=really-not-a-patch.png
+Content-Type: image/png
+Content-Transfer-Encoding: base64
+
+iVBORw0KGgoAAAANSUhEUgAAAjMAAAAfCAYAAAASo0ymAAABfGlDQ1BJQ0MgUHJvZmlsZQAAKJFj
+YGAqSSwoyGFhYGDIzSspCnJ3UoiIjFJgv8PAzcDDIMRgxSCemFxc4BgQ4MOAE3y7xsAIoi/rgsxK
+8/x506a1fP4WNq+ZclYlOrj1gQF3SmpxMgMDIweQnZxSnJwLZOcA2TrJBUUlQPYMIFu3vKQAxD4B
+ZIsUAR0IZN8BsdMh7A8gdhKYzcQCVhMS5AxkSwDZAkkQtgaInQ5hW4DYyRmJKUC2B8guiBvAgNPD
+RcHcwFLXkYC7SQa5OaUwO0ChxZOaFxoMcgcQyzB4MLgwKDCYMxgwWDLoMjiWpFaUgBQ65xdUFmWm
+Z5QoOAJDNlXBOT+3oLQktUhHwTMvWU9HwcjA0ACkDhRnEKM/B4FNZxQ7jxDLX8jAYKnMwMDcgxBL
+msbAsH0PA4PEKYSYyjwGBn5rBoZt5woSixLhDmf8xkKIX5xmbARh8zgxMLDe+///sxoDA/skBoa/
+E////73o//+/i4H2A+PsQA4AJHdp4IxrEg8AAAGcaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8
+eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA1LjQu
+MCI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1y
+ZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAg
+ICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIj4KICAgICAg
+ICAgPGV4aWY6UGl4ZWxYRGltZW5zaW9uPjU2MzwvZXhpZjpQaXhlbFhEaW1lbnNpb24+CiAgICAg
+ICAgIDxleGlmOlBpeGVsWURpbWVuc2lvbj4zMTwvZXhpZjpQaXhlbFlEaW1lbnNpb24+CiAgICAg
+IDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgpZ5vHZAAAS+ElE
+QVR4Ae2cC1QUV5rH/xnA1uZli02MiEHQqBMdTGB8ZIniA5xgfCTRycb1rBmdTHJGJ+toPBtzzGg2
+Rs3LdVTOYhKMcDgmDNHVNXKiGKJINCoqiXpUEFdB0CDKCtrSYp/Zr6q7muqmu+pWd7XgnFtH6Xrc
++h6/+92qr+69VQ9dqrvxd/CFE+AEOAFOgBPgBDiBB5TALx5Qu7nZnAAnwAlwApwAJ8AJiAR4MsMD
+gRPgBDgBToAT4AQeaAI8mXmgq48bzwlwApwAJ8AJcAI8meExwAlwApwAJ8AJcAIPNAGezDzQ1ceN
+5wQ4AU6AE+AEOIFgTwiamls87eb7OAFOgBPgBDgBToAT0I1ARHhXXWTxnhldMHIhnAAnwAlwApwA
+J9BRBIL+vOjfl7srt969577L63Zj1VGU/liHHn1j0I0xNbI1X8TRo5V4yNQLEQbGk7xawH6go/Sy
+W8hLBoqAL3EaKFtY5doaz6PkwCk8FN0H3f1oJx0d93r5wcpN13K2RhzbX4KKn4FH+vSAx65sXRX+
+YwgTYq5k72FUXqzChaoq1DQGI7Z3dwT6ai/oPbj/OJoNDyM6IsRvmL74odTe9LbPbwc7gQCDQZ9W
+5VFK0co52FKp7OW0N9di6sAI1B//G7YU3kHvEb+Gyah8jnTUWncYWdm7kf7GWrz0eIS0O+C/HaVX
+P8dsqPphL07dicFvxg6BQT/BnVSSfv76EqcdDcVafww5X1A7iRmGR/1oJ+pxrx9nT8z08sOT7IDv
+s9YiPzcf10InImlkfy9tLrD8Au5jABRY6w8j56vdbZIFfslxCG/bE5A1Idazqc2EpUVj/cwhfuvw
+xQ+l9qa3fX47eN8EWFC4cj4KKgdjadZiJATg5uUxUQ6JiEGYOQpm8X9bhiLtC4MRIcH2PCg4RBjv
+Mml7YgkOFRFGhnrMpQKHt6P06ubRbZzIy8f2radxVzeZnVmQfv76FKcdjSa4i2hBiL/NRDXu9ePs
+EZlefngUHuCdQcF0daOlRxcEeVUVYH5e9XbeA8aEF/HZZ5vw2Zo/wSyYqchPRz8csdbD70Zjt8kn
+P5Tam8726Ugu4KJaxam4TWixBUaVx8tk6vx3kSrps57HqtdWosI8Ge9/8BylMV4WR0u32WwICvLe
+7IWzjQnP4vPPn/UiKHC7O0qvfh4ZYOxB0lpCYb/N6Se5c0oKgL8a4rRjmNhATUhsQwblZsRsnnrc
+B4Az9PeD2eH7XjAQ/O67E7orFG8Dxm5iMnhNd+n3T6BWP9Tb2/2zvTNpkgb9PCYdOhjKINcxf4Y6
+YJQSquoTxdiVl4eTtwWrYjDzzYVIGyg+0zjNrC3NQ27JFUREGGC1Av/04qsY0ad9f1PD6d3IzdmJ
+k9cs4rlh5niMmTIL01PinLK0rHSE3trSTfikBMhIj8P3m5W5NJwrxqfr81AhsgP6Jk3GnDnP4VEp
+c7ReRM7H2agjpytqBM+/wZqPz0CA2NJiQMYfX8eIXu05KjFqvnwCuwv34shPZ3DNofexpIl4/l+m
+Y6BJ+13U7u9djEg0YR91L4sXL/MwzF/4KpJktjHp1eyvFee+244tW3ejWvTFiMeSnsZvZ09HQrir
+LyxxqsRNOsbkBxXWEgdV1D7WZhfjlqAkNgWzUyRtvv2qxr1mzmx2sPqhGvcOdZarJ1CwaQv2VV4X
+94TFDsfLf/ydS1wJB1iuG1rqw6He+0+A+Kn5a71cgjXritB7yh8oRmJl9jWh6JN1KL07DAvmP2vv
+UaKjavIEAbpykVnEssoaB6zl3HU2nP4auTsO42ZECt6YPzFgQ12q7c3dMMe2N/tY6s2LSI+7Wfhp
+iQO97fNotIadHoeZvJ3veluQSgk30Vps2ZiHq/1SkJ4UY99enYlz9lxEKmj/peLW+gqcPFWOmtuU
+0bgttqvFWPxRPiUyQHLaRExKG47elgvYlV2KZreymjbvs96WhjpUV5YiK1OZS/PpfCxebU9kho4Z
+h6eGRKH62E4sn7cOl5x4gtDFEEFJYATCRKe7wWAwiNuRkeHahvgc0Kr3rseuQ5QQ9R6MVOKcOiQG
+Fcd2Y/XCNahy6mUnbPf3CAookemWNA6TxsQD18qxYckK/CirODa9Wvy1onTDIqzOFRKZKKRmTEb6
+mEdFX0ovOLI00Q2NcariOpsf1InGGAe1peuwQkxkYpA+dSKG3iil+TKlKlYwHFaMey2cGXRREVY/
+2OKebsRVuzFvyXoxkXlsFF0PxgzGrZojFFeLcLih7fGK9brBWh/tvPX4tQr9+bH4a+gRjZZrtdj3
+5X40ygy1XS7DlkMXUA26Vjj2s8gTivrMRabfl1XWOGAt525D7Q+b6H6yDScrDXj+n1MDlsg49Sq2
+N2cp54o3+1jrzSlIZYWVH2sc6G2fivlMhxl6ZpjkIPmltzAvvb9Y+Fdb3sZHRRdwsqYJA2mSsLTE
+pMzCEnratF38Gr9/Z5u02+XXevOmuJ3+xgc0OdjRNTHzFTQ3W30OxI7SKziizMWC4vzdor8zaEJ1
+hshqFp7c/DY27C/H1oM1WDiWnrwMsXhp0WIqZ0PhX15BQcsYzKMnL+H27OvSf+p7WPVCNHo5ey5e
+xIQ9H2LpF2dw4mITEmT1pkVH34w/4Z0ZT4in/Lr3Oiz/ohyFJeeROMkeG0x6NfhrOVeI7GOUNZvH
+YcWqWYhxZNy/faEGV++1J6RcH+yeMvkhE6es9woKs8updBTmf/guknrS6uSn8Om/LcNBeT4mk8ey
+qhr3Gjiz6ANY/WCMe+rf2/Gf+aQ6CnNXrUZKL3vlPjehGAvezkPWl98jef5ocS6L1uuGcn3IvDUM
+wpLPN8l2yFZ158for3EQpowyYsOhYpRdnoE0R+/22ZLvROOmpSU75vcwypO5xMxFdo7vq6xxwFqu
+zZKudOuoKs2iB4QjQOhwLP/gtbae7rZiuq6ptjeZNmX7tNebTLSHVe38lOPAP/vY35X24IrCLt2S
+mcRf9XOq6ZtIs8iLahHipc/Aamt1lm2/Yp/auicnE5HPP4PEuBiYzCaEh0tjLu3PYN3TEXoVuViq
+cbyGrDdPxNOy5CHp2QyE7f8UNxvc72RW2Mm1ihOA29+qWUlQfmR6BIaLJ1D4zQn878+30KVLF9xt
+qGcX0K6k0J1jRMYEeyIjHH40JR19KZmpOHwSFkpmhBrUplfd35qfjguqkD57ijOREbaDwmNpsLP9
+olgf7Yt73aPND0BRr+UmLguaaHhxmJDICEtQLMZNiMfBHRfs2378VY57QbA6Zyb1rH6wxr3lOs6K
+TeA6DnyTgzN37deGLtRHKw7FVV+B0Plrf0NG23VDsT6YnJUX0osfu79Dxv8GOLQNRSWVSBPe2rHV
+YB9dc4HhGDHQca3UxM/uj75c5Iw8rDPHgdbrJA3Hf/UBVogqB2P5XymR8Tys4MEo/3eptzcV+3yo
+N0WrWTnLhCjGgT/2hfbDw/7fymWWtq3qkMwIN7EYmGXzLIK62qentkJ7Dmbsn46Zo05Rd+kZFGyk
+/w5bU//1Lcwa21/hjYI2p3xZ018vAxdqYOK3D4mXS1sLN6M3OVFRXUe3mUEuPTDSJCqX8j44fLqA
+es8KhYsf5VKx8TCRIS3/d8cHSfJTTOguD1RbM3V5uy5a9ar5GxxiT+mMXdVCmaE+XE1V3GL3g0Gv
+ozLND0e7xMG9VuHc+7OocWaygtUPKscU91KQmyktratDg8yIoQPiYe1pdvJib78M9SHTw7qqJz8w
++GtIGIHU0G2UwBSjlpIZ0/mDKCNj+05NRS/JaA38hIRWz+u4ZILir4Y4YIoXj8rOYN/RK5g98hGP
+Rzt+pwf7NNUbgwesnEVRDHHgo33ig7jR5P0lIgZXlIqo3QGUznU91jZ87bpf61aQCWl/eBfjZjXh
+Wn0dzh7dg62F5diXuxHJIz6ENPKkVaxq+UDpVeJCx8ShePfx+LvN4li4uW9vl0SmzYe7NODkx0Jv
+qP2PkMiEplD36xxn96u16r/x2oqdfghuQYvwgCx1GXUJx2O0WUH+ifb6rNe7v9INv5U1b/YLnAON
+L34o6XUcu3NbNrmIVAUbJZAOvQH/8c6ZSTWrH1SOKe4lZt3HYslb45RN0Np+JdnKUjUe1YcfWPyl
+F55TnomnyfblKKtqRJ/9B8hWI9JGDmizWfKRSZ7jNOmcNin+rZE898ubU6B0zL2A+/WPtZxTMBA2
+6hW8P6Mr3l9I8602voeEuL86hyllxdhXJRvYz1AsqWgf6RIXLfWmpE2yXY2zXIZkg3yftC4d02Sf
+EVP/YxOmSjIC8KtpArBe+g1B9ueYkGAPF2ubVbzxBRkj0CtuEFJnvI7X0qJItZib+2VCR+n1arQx
+CoNC6ei1gzgpe+y8VLJXfBvIZHTPNe/Zh5munUKdkED7utju2S8w/eKciQxNtcS3+d/6KpHOE+ry
+OkrLapwyGs+WoYK2zMMG2IcCNOtV9ze6n314c9fXB8QhB6dyetK0WKVW17ZXlzXNfqhoNXRFNBW5
+tX+fbPJ1E04d9n+ISdCsGPeiaeqcxWJqf1j9YI17Q6jIBZXf4Uf5TFfRDrf6DeB1Q81tUA+0+NTp
+b7vU4i8ZlTBivPhCwPaNK7H5EA24xY7Hk455RaLNGuWp++lDCWNvDDXTeTdui30/LhJY44C1nEx4
+j2jqATA9gQULhCTYguwlmaj153Kg5IdMr7Cq3t7osztK9uldbz7wc3PJddMn+yy4dO4sTp87j0Z/
+7l2ulrhsud8tXQ66bFBW508s2JrP40BJFVqNIWi9Qm/R0HL820JEVhthae2G4eNHoyd1X53bthSr
+C7siffpY/DLOhObKE8gvEl7JHIwwD7mPKEjhT0fpVTBJdsiM0S8Mw57ccmQtXoHrr05ExNVjyN4h
+8IlCxuj+srLCagT6D6LErqYWGz7OwoRkupHTNWzI+AntXkF2O9F10xGM1afykEnjeKm/DMdPO3Kw
+p1KYgeDfUpa7DDmWlzHIeAVbc4tFYRmjHU+LmvWq+2tKfg6TzKXYdSof81bWYm76UIS0/Izvs7ch
+ir4wPduPL+d6JaHZD6+S7AeC4jBhagzKqN5XvJeFuVOScP2Hv2F7W16oIqD9Yda4t5+pzrm9Bg97
+mP1gjHuaN/Tiqyko21iKtQsXY9JLk9GPPlF/4+oZFO0oxZ0xC7H+ZZovQove1w0P3ins0osfu7+i
+MT0T8UwsUFBzXZxDlJw+0rULXwM/Bef8PBSKfv1p7JkmK/9l5S2MGRBGHUiPY/KkJ+jxhzEOmMvJ
+TBWzS/qca+IsLEg7h7VF5Vi6thhZi8Y5O45lpRlWlfygeyPj/c2pSMk+3euNlbPTOuUVX+yjN5Iz
+V68BvaQcsC//sycz3UOc49OungoZhudUSz4B2Fp/kj5v7TqEUX1oJ3IOCdKiaLIoJTMU85GxifS0
+UYw9X+Vhj6QoNB6z//x7nyZxdZRee0+FOpeYsfPwhiUTH1F3ccHG/3J4HI+5y15Hon1mo0RB/H38
+hdcx7cYn2H7sCLZXHrEfG/o0JTPyySoup7TfoGD83bKXUf/OZpQV5tF/oUg80tMM2FN0hiZu+76Y
+B0RR1/dm7HOImDTvPaRK3xLyQa+6vyZMX/U+IrPX0TyrUmTT6/D2JQZzI+XZL1ucOk5W/tHkB5ve
+gdMWY279h8g+dATZmUK9RuGpUTQBmF61lb62rWyU61HWuJfOUucslVT+ZfWDNe57jpyDVcEmrMnc
+iV1fbG5THhqDaU887Nxmv26w1YdTMOOKXvxY/bWbZcTwKSkoyBRiPh4TnnyknbXs8gLDhWayI2nu
+Msy4uw4FdM3aVUkmmkORQcmMsLDGAWu5IMfVq6usVztx5kJMOruIHnjy8GXZUMxOFrqKtC7KfrC2
+N1b72OuNzQ9Wfqz3Lc32Ob6kLSQzkarzG9l8ci/10KW6G39339nU7D645l4i0Ns2WCy36UuoNljv
+BcFkivCSSOltR0fppcze0kTdb9T3Rf/Ce5p8fHrQysOKxkZ7b4zR5J/Oqu1vY8UOA5Z+thRx5EvT
+PRvN+aC30IRrZLtFP71y0VZLI+gNfhho+NJIyR119AV40d8PS2Oj+GhgECbKeWQXYJd0Es/qB3vc
+E+uGJqpcA4KDDAj3CKfj2q9O2GRiWPyVFVdd1VueqkJNBVjjgLWcJuWdurC+9aY/P//tiwj3fwqJ
+UIWdNJnp1NHFjfNAwJ7MAG9mvgvpzVAPxfguToAT4AQ4AU7ASUCvZIZ9mMmpmq9wAu0J3Gu1z9Bk
+famovQS+hxPgBDgBToAT8I0A75nxjRs/y42ApaEGV24Go08CfYzP7Rjf5AQ4AU6AE+AEPBHQq2fG
+YzLjSSHfxwlwApwAJ8AJcAKcQGck8IvOaBS3iRPgBDgBToAT4AQ4AVYCPJlhJcXLcQKcACfACXAC
+nECnJMCTmU5ZLdwoToAT4AQ4AU6AE2AlwJMZVlK8HCfACXACnAAnwAl0SgI8memU1cKN4gQ4AU6A
+E+AEOAFWAv8PxvdR0yK8jugAAAAASUVORK5CYII=
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=--
diff --git a/spec/fixtures/emails/merge_request_with_conflicting_patch.eml b/spec/fixtures/emails/merge_request_with_conflicting_patch.eml
new file mode 100644
index 00000000000..ddfdfe9e24a
--- /dev/null
+++ b/spec/fixtures/emails/merge_request_with_conflicting_patch.eml
@@ -0,0 +1,45 @@
+From: "Jake the Dog" <jake@adventuretime.ooo>
+To: incoming+gitlabhq/gitlabhq+merge-request+auth_token@appmail.adventuretime.ooo
+Subject: feature
+Date: Wed, 31 Oct 2018 17:27:52 +0100
+X-Mailer: MailMate (1.12r5523)
+Message-ID: <7BE7C2E6-F0D9-4C85-9F55-B2A4BA01BFEC@gitlab.com>
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_="
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+
+This does not apply
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+Content-Disposition: attachment;
+ filename=0002-This-does-not-apply-to-the-feature-branch.patch
+Content-Transfer-Encoding: quoted-printable
+
+=46rom 00c68c2b4f954370ce82a1162bc29c13f524897e Mon Sep 17 00:00:00 2001
+From: Patch user <patchuser@gitlab.org>
+Date: Mon, 22 Oct 2018 11:05:48 +0200
+Subject: [PATCH] This does not apply to the `feature` branch
+
+---
+ files/ruby/feature.rb | 5 +++++
+ 1 file changed, 5 insertions(+)
+ create mode 100644 files/ruby/feature.rb
+
+diff --git a/files/ruby/feature.rb b/files/ruby/feature.rb
+new file mode 100644
+index 0000000..fef26e4
+--- /dev/null
++++ b/files/ruby/feature.rb
+@@ -0,0 +1,5 @@
++class Feature
++ def bar
++ puts 'foo'
++ end
++end
+-- =
+
+2.19.1
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=--
diff --git a/spec/fixtures/emails/merge_request_with_patch_and_target_branch.eml b/spec/fixtures/emails/merge_request_with_patch_and_target_branch.eml
new file mode 100644
index 00000000000..965658721cd
--- /dev/null
+++ b/spec/fixtures/emails/merge_request_with_patch_and_target_branch.eml
@@ -0,0 +1,44 @@
+From: "Jake the Dog" <jake@adventuretime.ooo>
+To: incoming+gitlabhq/gitlabhq+merge-request+auth_token@appmail.adventuretime.ooo
+Subject: new-branch-with-a-patch
+Date: Wed, 24 Oct 2018 16:39:49 +0200
+X-Mailer: MailMate (1.12r5523)
+Message-ID: <F1F36291-728D-4E8F-AFEB-C398B8D9BB4E@gitlab.com>
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_="
+
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+
+This applies nicely to a branch freshly created from the root-ref
+
+The other attachments in this email are ignored
+
+/target_branch with-codeowners
+
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+Content-Disposition: attachment; filename=0001-A-commit-from-a-patch.patch
+Content-Transfer-Encoding: quoted-printable
+
+=46rom 3fee0042e610fb3563e4379e316704cb1210f3de Mon Sep 17 00:00:00 2001
+From: Patch user <patchuser@gitlab.org>
+Date: Thu, 18 Oct 2018 13:40:35 +0200
+Subject: [PATCH] A commit from a patch
+
+---
+ README | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/README b/README
+index 3742e48..e40a3b9 100644
+--- a/README
++++ b/README
+@@ -1 +1,3 @@
+ Sample repo for testing gitlab features
++
++This was applied in a patch!
+-- =
+
+2.19.1
diff --git a/spec/fixtures/emails/valid_merge_request_with_patch.eml b/spec/fixtures/emails/valid_merge_request_with_patch.eml
new file mode 100644
index 00000000000..143fa77d1fa
--- /dev/null
+++ b/spec/fixtures/emails/valid_merge_request_with_patch.eml
@@ -0,0 +1,151 @@
+From: "Jake the Dog" <jake@adventuretime.ooo>
+To: incoming+gitlabhq/gitlabhq+merge-request+auth_token@appmail.adventuretime.ooo
+Subject: new-branch-with-a-patch
+Date: Wed, 24 Oct 2018 16:39:49 +0200
+X-Mailer: MailMate (1.12r5523)
+Message-ID: <F1F36291-728D-4E8F-AFEB-C398B8D9BB4E@gitlab.com>
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_="
+
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+
+This applies nicely to a branch freshly created from the root-ref
+
+The other attachments in this email are ignored
+
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+Content-Disposition: attachment; filename=0001-A-commit-from-a-patch.patch
+Content-Transfer-Encoding: quoted-printable
+
+=46rom 3fee0042e610fb3563e4379e316704cb1210f3de Mon Sep 17 00:00:00 2001
+From: Patch user <patchuser@gitlab.org>
+Date: Thu, 18 Oct 2018 13:40:35 +0200
+Subject: [PATCH] A commit from a patch
+
+---
+ README | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/README b/README
+index 3742e48..e40a3b9 100644
+--- a/README
++++ b/README
+@@ -1 +1,3 @@
+ Sample repo for testing gitlab features
++
++This was applied in a patch!
+-- =
+
+2.19.1
+
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+Content-Disposition: attachment; filename=really-not-a-patch.png
+Content-Type: image/png
+Content-Transfer-Encoding: base64
+
+iVBORw0KGgoAAAANSUhEUgAAAjMAAAAfCAYAAAASo0ymAAABfGlDQ1BJQ0MgUHJvZmlsZQAAKJFj
+YGAqSSwoyGFhYGDIzSspCnJ3UoiIjFJgv8PAzcDDIMRgxSCemFxc4BgQ4MOAE3y7xsAIoi/rgsxK
+8/x506a1fP4WNq+ZclYlOrj1gQF3SmpxMgMDIweQnZxSnJwLZOcA2TrJBUUlQPYMIFu3vKQAxD4B
+ZIsUAR0IZN8BsdMh7A8gdhKYzcQCVhMS5AxkSwDZAkkQtgaInQ5hW4DYyRmJKUC2B8guiBvAgNPD
+RcHcwFLXkYC7SQa5OaUwO0ChxZOaFxoMcgcQyzB4MLgwKDCYMxgwWDLoMjiWpFaUgBQ65xdUFmWm
+Z5QoOAJDNlXBOT+3oLQktUhHwTMvWU9HwcjA0ACkDhRnEKM/B4FNZxQ7jxDLX8jAYKnMwMDcgxBL
+msbAsH0PA4PEKYSYyjwGBn5rBoZt5woSixLhDmf8xkKIX5xmbARh8zgxMLDe+///sxoDA/skBoa/
+E////73o//+/i4H2A+PsQA4AJHdp4IxrEg8AAAGcaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8
+eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA1LjQu
+MCI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1y
+ZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAg
+ICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIj4KICAgICAg
+ICAgPGV4aWY6UGl4ZWxYRGltZW5zaW9uPjU2MzwvZXhpZjpQaXhlbFhEaW1lbnNpb24+CiAgICAg
+ICAgIDxleGlmOlBpeGVsWURpbWVuc2lvbj4zMTwvZXhpZjpQaXhlbFlEaW1lbnNpb24+CiAgICAg
+IDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgpZ5vHZAAAS+ElE
+QVR4Ae2cC1QUV5rH/xnA1uZli02MiEHQqBMdTGB8ZIniA5xgfCTRycb1rBmdTHJGJ+toPBtzzGg2
+Rs3LdVTOYhKMcDgmDNHVNXKiGKJINCoqiXpUEFdB0CDKCtrSYp/Zr6q7muqmu+pWd7XgnFtH6Xrc
++h6/+92qr+69VQ9dqrvxd/CFE+AEOAFOgBPgBDiBB5TALx5Qu7nZnAAnwAlwApwAJ8AJiAR4MsMD
+gRPgBDgBToAT4AQeaAI8mXmgq48bzwlwApwAJ8AJcAI8meExwAlwApwAJ8AJcAIPNAGezDzQ1ceN
+5wQ4AU6AE+AEOIFgTwiamls87eb7OAFOgBPgBDgBToAT0I1ARHhXXWTxnhldMHIhnAAnwAlwApwA
+J9BRBIL+vOjfl7srt969577L63Zj1VGU/liHHn1j0I0xNbI1X8TRo5V4yNQLEQbGk7xawH6go/Sy
+W8hLBoqAL3EaKFtY5doaz6PkwCk8FN0H3f1oJx0d93r5wcpN13K2RhzbX4KKn4FH+vSAx65sXRX+
+YwgTYq5k72FUXqzChaoq1DQGI7Z3dwT6ai/oPbj/OJoNDyM6IsRvmL74odTe9LbPbwc7gQCDQZ9W
+5VFK0co52FKp7OW0N9di6sAI1B//G7YU3kHvEb+Gyah8jnTUWncYWdm7kf7GWrz0eIS0O+C/HaVX
+P8dsqPphL07dicFvxg6BQT/BnVSSfv76EqcdDcVafww5X1A7iRmGR/1oJ+pxrx9nT8z08sOT7IDv
+s9YiPzcf10InImlkfy9tLrD8Au5jABRY6w8j56vdbZIFfslxCG/bE5A1Idazqc2EpUVj/cwhfuvw
+xQ+l9qa3fX47eN8EWFC4cj4KKgdjadZiJATg5uUxUQ6JiEGYOQpm8X9bhiLtC4MRIcH2PCg4RBjv
+Mml7YgkOFRFGhnrMpQKHt6P06ubRbZzIy8f2radxVzeZnVmQfv76FKcdjSa4i2hBiL/NRDXu9ePs
+EZlefngUHuCdQcF0daOlRxcEeVUVYH5e9XbeA8aEF/HZZ5vw2Zo/wSyYqchPRz8csdbD70Zjt8kn
+P5Tam8726Ugu4KJaxam4TWixBUaVx8tk6vx3kSrps57HqtdWosI8Ge9/8BylMV4WR0u32WwICvLe
+7IWzjQnP4vPPn/UiKHC7O0qvfh4ZYOxB0lpCYb/N6Se5c0oKgL8a4rRjmNhATUhsQwblZsRsnnrc
+B4Az9PeD2eH7XjAQ/O67E7orFG8Dxm5iMnhNd+n3T6BWP9Tb2/2zvTNpkgb9PCYdOhjKINcxf4Y6
+YJQSquoTxdiVl4eTtwWrYjDzzYVIGyg+0zjNrC3NQ27JFUREGGC1Av/04qsY0ad9f1PD6d3IzdmJ
+k9cs4rlh5niMmTIL01PinLK0rHSE3trSTfikBMhIj8P3m5W5NJwrxqfr81AhsgP6Jk3GnDnP4VEp
+c7ReRM7H2agjpytqBM+/wZqPz0CA2NJiQMYfX8eIXu05KjFqvnwCuwv34shPZ3DNofexpIl4/l+m
+Y6BJ+13U7u9djEg0YR91L4sXL/MwzF/4KpJktjHp1eyvFee+244tW3ejWvTFiMeSnsZvZ09HQrir
+LyxxqsRNOsbkBxXWEgdV1D7WZhfjlqAkNgWzUyRtvv2qxr1mzmx2sPqhGvcOdZarJ1CwaQv2VV4X
+94TFDsfLf/ydS1wJB1iuG1rqw6He+0+A+Kn5a71cgjXritB7yh8oRmJl9jWh6JN1KL07DAvmP2vv
+UaKjavIEAbpykVnEssoaB6zl3HU2nP4auTsO42ZECt6YPzFgQ12q7c3dMMe2N/tY6s2LSI+7Wfhp
+iQO97fNotIadHoeZvJ3veluQSgk30Vps2ZiHq/1SkJ4UY99enYlz9lxEKmj/peLW+gqcPFWOmtuU
+0bgttqvFWPxRPiUyQHLaRExKG47elgvYlV2KZreymjbvs96WhjpUV5YiK1OZS/PpfCxebU9kho4Z
+h6eGRKH62E4sn7cOl5x4gtDFEEFJYATCRKe7wWAwiNuRkeHahvgc0Kr3rseuQ5QQ9R6MVOKcOiQG
+Fcd2Y/XCNahy6mUnbPf3CAookemWNA6TxsQD18qxYckK/CirODa9Wvy1onTDIqzOFRKZKKRmTEb6
+mEdFX0ovOLI00Q2NcariOpsf1InGGAe1peuwQkxkYpA+dSKG3iil+TKlKlYwHFaMey2cGXRREVY/
+2OKebsRVuzFvyXoxkXlsFF0PxgzGrZojFFeLcLih7fGK9brBWh/tvPX4tQr9+bH4a+gRjZZrtdj3
+5X40ygy1XS7DlkMXUA26Vjj2s8gTivrMRabfl1XWOGAt525D7Q+b6H6yDScrDXj+n1MDlsg49Sq2
+N2cp54o3+1jrzSlIZYWVH2sc6G2fivlMhxl6ZpjkIPmltzAvvb9Y+Fdb3sZHRRdwsqYJA2mSsLTE
+pMzCEnratF38Gr9/Z5u02+XXevOmuJ3+xgc0OdjRNTHzFTQ3W30OxI7SKziizMWC4vzdor8zaEJ1
+hshqFp7c/DY27C/H1oM1WDiWnrwMsXhp0WIqZ0PhX15BQcsYzKMnL+H27OvSf+p7WPVCNHo5ey5e
+xIQ9H2LpF2dw4mITEmT1pkVH34w/4Z0ZT4in/Lr3Oiz/ohyFJeeROMkeG0x6NfhrOVeI7GOUNZvH
+YcWqWYhxZNy/faEGV++1J6RcH+yeMvkhE6es9woKs8updBTmf/guknrS6uSn8Om/LcNBeT4mk8ey
+qhr3Gjiz6ANY/WCMe+rf2/Gf+aQ6CnNXrUZKL3vlPjehGAvezkPWl98jef5ocS6L1uuGcn3IvDUM
+wpLPN8l2yFZ158for3EQpowyYsOhYpRdnoE0R+/22ZLvROOmpSU75vcwypO5xMxFdo7vq6xxwFqu
+zZKudOuoKs2iB4QjQOhwLP/gtbae7rZiuq6ptjeZNmX7tNebTLSHVe38lOPAP/vY35X24IrCLt2S
+mcRf9XOq6ZtIs8iLahHipc/Aamt1lm2/Yp/auicnE5HPP4PEuBiYzCaEh0tjLu3PYN3TEXoVuViq
+cbyGrDdPxNOy5CHp2QyE7f8UNxvc72RW2Mm1ihOA29+qWUlQfmR6BIaLJ1D4zQn878+30KVLF9xt
+qGcX0K6k0J1jRMYEeyIjHH40JR19KZmpOHwSFkpmhBrUplfd35qfjguqkD57ijOREbaDwmNpsLP9
+olgf7Yt73aPND0BRr+UmLguaaHhxmJDICEtQLMZNiMfBHRfs2378VY57QbA6Zyb1rH6wxr3lOs6K
+TeA6DnyTgzN37deGLtRHKw7FVV+B0Plrf0NG23VDsT6YnJUX0osfu79Dxv8GOLQNRSWVSBPe2rHV
+YB9dc4HhGDHQca3UxM/uj75c5Iw8rDPHgdbrJA3Hf/UBVogqB2P5XymR8Tys4MEo/3eptzcV+3yo
+N0WrWTnLhCjGgT/2hfbDw/7fymWWtq3qkMwIN7EYmGXzLIK62qentkJ7Dmbsn46Zo05Rd+kZFGyk
+/w5bU//1Lcwa21/hjYI2p3xZ018vAxdqYOK3D4mXS1sLN6M3OVFRXUe3mUEuPTDSJCqX8j44fLqA
+es8KhYsf5VKx8TCRIS3/d8cHSfJTTOguD1RbM3V5uy5a9ar5GxxiT+mMXdVCmaE+XE1V3GL3g0Gv
+ozLND0e7xMG9VuHc+7OocWaygtUPKscU91KQmyktratDg8yIoQPiYe1pdvJib78M9SHTw7qqJz8w
++GtIGIHU0G2UwBSjlpIZ0/mDKCNj+05NRS/JaA38hIRWz+u4ZILir4Y4YIoXj8rOYN/RK5g98hGP
+Rzt+pwf7NNUbgwesnEVRDHHgo33ig7jR5P0lIgZXlIqo3QGUznU91jZ87bpf61aQCWl/eBfjZjXh
+Wn0dzh7dg62F5diXuxHJIz6ENPKkVaxq+UDpVeJCx8ShePfx+LvN4li4uW9vl0SmzYe7NODkx0Jv
+qP2PkMiEplD36xxn96u16r/x2oqdfghuQYvwgCx1GXUJx2O0WUH+ifb6rNe7v9INv5U1b/YLnAON
+L34o6XUcu3NbNrmIVAUbJZAOvQH/8c6ZSTWrH1SOKe4lZt3HYslb45RN0Np+JdnKUjUe1YcfWPyl
+F55TnomnyfblKKtqRJ/9B8hWI9JGDmizWfKRSZ7jNOmcNin+rZE898ubU6B0zL2A+/WPtZxTMBA2
+6hW8P6Mr3l9I8602voeEuL86hyllxdhXJRvYz1AsqWgf6RIXLfWmpE2yXY2zXIZkg3yftC4d02Sf
+EVP/YxOmSjIC8KtpArBe+g1B9ueYkGAPF2ubVbzxBRkj0CtuEFJnvI7X0qJItZib+2VCR+n1arQx
+CoNC6ei1gzgpe+y8VLJXfBvIZHTPNe/Zh5munUKdkED7utju2S8w/eKciQxNtcS3+d/6KpHOE+ry
+OkrLapwyGs+WoYK2zMMG2IcCNOtV9ze6n314c9fXB8QhB6dyetK0WKVW17ZXlzXNfqhoNXRFNBW5
+tX+fbPJ1E04d9n+ISdCsGPeiaeqcxWJqf1j9YI17Q6jIBZXf4Uf5TFfRDrf6DeB1Q81tUA+0+NTp
+b7vU4i8ZlTBivPhCwPaNK7H5EA24xY7Hk455RaLNGuWp++lDCWNvDDXTeTdui30/LhJY44C1nEx4
+j2jqATA9gQULhCTYguwlmaj153Kg5IdMr7Cq3t7osztK9uldbz7wc3PJddMn+yy4dO4sTp87j0Z/
+7l2ulrhsud8tXQ66bFBW508s2JrP40BJFVqNIWi9Qm/R0HL820JEVhthae2G4eNHoyd1X53bthSr
+C7siffpY/DLOhObKE8gvEl7JHIwwD7mPKEjhT0fpVTBJdsiM0S8Mw57ccmQtXoHrr05ExNVjyN4h
+8IlCxuj+srLCagT6D6LErqYWGz7OwoRkupHTNWzI+AntXkF2O9F10xGM1afykEnjeKm/DMdPO3Kw
+p1KYgeDfUpa7DDmWlzHIeAVbc4tFYRmjHU+LmvWq+2tKfg6TzKXYdSof81bWYm76UIS0/Izvs7ch
+ir4wPduPL+d6JaHZD6+S7AeC4jBhagzKqN5XvJeFuVOScP2Hv2F7W16oIqD9Yda4t5+pzrm9Bg97
+mP1gjHuaN/Tiqyko21iKtQsXY9JLk9GPPlF/4+oZFO0oxZ0xC7H+ZZovQove1w0P3ins0osfu7+i
+MT0T8UwsUFBzXZxDlJw+0rULXwM/Bef8PBSKfv1p7JkmK/9l5S2MGRBGHUiPY/KkJ+jxhzEOmMvJ
+TBWzS/qca+IsLEg7h7VF5Vi6thhZi8Y5O45lpRlWlfygeyPj/c2pSMk+3euNlbPTOuUVX+yjN5Iz
+V68BvaQcsC//sycz3UOc49OungoZhudUSz4B2Fp/kj5v7TqEUX1oJ3IOCdKiaLIoJTMU85GxifS0
+UYw9X+Vhj6QoNB6z//x7nyZxdZRee0+FOpeYsfPwhiUTH1F3ccHG/3J4HI+5y15Hon1mo0RB/H38
+hdcx7cYn2H7sCLZXHrEfG/o0JTPyySoup7TfoGD83bKXUf/OZpQV5tF/oUg80tMM2FN0hiZu+76Y
+B0RR1/dm7HOImDTvPaRK3xLyQa+6vyZMX/U+IrPX0TyrUmTT6/D2JQZzI+XZL1ucOk5W/tHkB5ve
+gdMWY279h8g+dATZmUK9RuGpUTQBmF61lb62rWyU61HWuJfOUucslVT+ZfWDNe57jpyDVcEmrMnc
+iV1fbG5THhqDaU887Nxmv26w1YdTMOOKXvxY/bWbZcTwKSkoyBRiPh4TnnyknbXs8gLDhWayI2nu
+Msy4uw4FdM3aVUkmmkORQcmMsLDGAWu5IMfVq6usVztx5kJMOruIHnjy8GXZUMxOFrqKtC7KfrC2
+N1b72OuNzQ9Wfqz3Lc32Ob6kLSQzkarzG9l8ci/10KW6G39339nU7D645l4i0Ns2WCy36UuoNljv
+BcFkivCSSOltR0fppcze0kTdb9T3Rf/Ce5p8fHrQysOKxkZ7b4zR5J/Oqu1vY8UOA5Z+thRx5EvT
+PRvN+aC30IRrZLtFP71y0VZLI+gNfhho+NJIyR119AV40d8PS2Oj+GhgECbKeWQXYJd0Es/qB3vc
+E+uGJqpcA4KDDAj3CKfj2q9O2GRiWPyVFVdd1VueqkJNBVjjgLWcJuWdurC+9aY/P//tiwj3fwqJ
+UIWdNJnp1NHFjfNAwJ7MAG9mvgvpzVAPxfguToAT4AQ4AU7ASUCvZIZ9mMmpmq9wAu0J3Gu1z9Bk
+famovQS+hxPgBDgBToAT8I0A75nxjRs/y42ApaEGV24Go08CfYzP7Rjf5AQ4AU6AE+AEPBHQq2fG
+YzLjSSHfxwlwApwAJ8AJcAKcQGck8IvOaBS3iRPgBDgBToAT4AQ4AVYCPJlhJcXLcQKcACfACXAC
+nECnJMCTmU5ZLdwoToAT4AQ4AU6AE2AlwJMZVlK8HCfACXACnAAnwAl0SgI8memU1cKN4gQ4AU6A
+E+AEOAFWAv8PxvdR0yK8jugAAAAASUVORK5CYII=
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=--
diff --git a/spec/fixtures/patchfiles/0001-A-commit-from-a-patch.patch b/spec/fixtures/patchfiles/0001-A-commit-from-a-patch.patch
new file mode 100644
index 00000000000..cc38682a0ab
--- /dev/null
+++ b/spec/fixtures/patchfiles/0001-A-commit-from-a-patch.patch
@@ -0,0 +1,19 @@
+From 3fee0042e610fb3563e4379e316704cb1210f3de Mon Sep 17 00:00:00 2001
+From: Patch User <patchuser@gitlab.org>
+Date: Thu, 18 Oct 2018 13:40:35 +0200
+Subject: [PATCH] A commit from a patch
+
+---
+ README | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/README b/README
+index 3742e48..e40a3b9 100644
+--- a/README
++++ b/README
+@@ -1 +1,3 @@
+ Sample repo for testing gitlab features
++
++This was applied in a patch!
+--
+2.19.1
diff --git a/spec/fixtures/patchfiles/0001-This-does-not-apply-to-the-feature-branch.patch b/spec/fixtures/patchfiles/0001-This-does-not-apply-to-the-feature-branch.patch
new file mode 100644
index 00000000000..905002ae898
--- /dev/null
+++ b/spec/fixtures/patchfiles/0001-This-does-not-apply-to-the-feature-branch.patch
@@ -0,0 +1,23 @@
+From 00c68c2b4f954370ce82a1162bc29c13f524897e Mon Sep 17 00:00:00 2001
+From: Patch User <patchuser@gitlab.org>
+Date: Mon, 22 Oct 2018 11:05:48 +0200
+Subject: [PATCH] This does not apply to the `feature` branch
+
+---
+ files/ruby/feature.rb | 5 +++++
+ 1 file changed, 5 insertions(+)
+ create mode 100644 files/ruby/feature.rb
+
+diff --git a/files/ruby/feature.rb b/files/ruby/feature.rb
+new file mode 100644
+index 0000000..fef26e4
+--- /dev/null
++++ b/files/ruby/feature.rb
+@@ -0,0 +1,5 @@
++class Feature
++ def bar
++ puts 'foo'
++ end
++end
+--
+2.19.1
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index 1c216b3fe97..f709f152c92 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -3,63 +3,13 @@ require 'spec_helper'
describe BlobHelper do
include TreeHelper
- let(:blob_name) { 'test.lisp' }
- let(:no_context_content) { ":type \"assem\"))" }
- let(:blob_content) { "(make-pathname :defaults name\n#{no_context_content}" }
- let(:split_content) { blob_content.split("\n") }
- let(:multiline_content) do
- %q(
- def test(input):
- """This is line 1 of a multi-line comment.
- This is line 2.
- """
- )
- end
-
describe '#highlight' do
- it 'returns plaintext for unknown lexer context' do
- result = helper.highlight(blob_name, no_context_content)
- expect(result).to eq(%[<pre class="code highlight"><code><span id="LC1" class="line" lang="">:type "assem"))</span></code></pre>])
+ it 'wraps highlighted content' do
+ expect(helper.highlight('test.rb', '52')).to eq(%q[<pre class="code highlight"><code><span id="LC1" class="line" lang="ruby"><span class="mi">52</span></span></code></pre>])
end
- it 'returns plaintext for long blobs' do
- stub_const('Blob::MAXIMUM_TEXT_HIGHLIGHT_SIZE', 1)
- result = helper.highlight(blob_name, blob_content)
-
- expect(result).to eq(%[<pre class="code highlight"><code><span id="LC1" class="line" lang="">(make-pathname :defaults name</span>\n<span id="LC2" class="line" lang="">:type "assem"))</span></code></pre>])
- end
-
- it 'highlights single block' do
- expected = %Q[<pre class="code highlight"><code><span id="LC1" class="line" lang="common_lisp"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span>
-<span id="LC2" class="line" lang="common_lisp"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span></code></pre>]
-
- expect(helper.highlight(blob_name, blob_content)).to eq(expected)
- end
-
- it 'highlights multi-line comments' do
- result = helper.highlight(blob_name, multiline_content)
- html = Nokogiri::HTML(result)
- lines = html.search('.s')
- expect(lines.count).to eq(3)
- expect(lines[0].text).to eq('"""This is line 1 of a multi-line comment.')
- expect(lines[1].text).to eq(' This is line 2.')
- expect(lines[2].text).to eq(' """')
- end
-
- context 'diff highlighting' do
- let(:blob_name) { 'test.diff' }
- let(:blob_content) { "+aaa\n+bbb\n- ccc\n ddd\n"}
- let(:expected) do
- %q(<pre class="code highlight"><code><span id="LC1" class="line" lang="diff"><span class="gi">+aaa</span></span>
-<span id="LC2" class="line" lang="diff"><span class="gi">+bbb</span></span>
-<span id="LC3" class="line" lang="diff"><span class="gd">- ccc</span></span>
-<span id="LC4" class="line" lang="diff"> ddd</span></code></pre>)
- end
-
- it 'highlights each line properly' do
- result = helper.highlight(blob_name, blob_content)
- expect(result).to eq(expected)
- end
+ it 'handles plain version' do
+ expect(helper.highlight('test.rb', '52', plain: true)).to eq(%q[<pre class="code highlight"><code><span id="LC1" class="line" lang="">52</span></code></pre>])
end
end
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index 466e018d68c..8d0679e5699 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -2,18 +2,18 @@ require 'spec_helper'
describe EventsHelper do
describe '#event_commit_title' do
- let(:message) { "foo & bar " + "A" * 70 + "\n" + "B" * 80 }
+ let(:message) { 'foo & bar ' + 'A' * 70 + '\n' + 'B' * 80 }
subject { helper.event_commit_title(message) }
- it "returns the first line, truncated to 70 chars" do
+ it 'returns the first line, truncated to 70 chars' do
is_expected.to eq(message[0..66] + "...")
end
- it "is not html-safe" do
+ it 'is not html-safe' do
is_expected.not_to be_a(ActiveSupport::SafeBuffer)
end
- it "handles empty strings" do
+ it 'handles empty strings' do
expect(helper.event_commit_title("")).to eq("")
end
@@ -22,7 +22,7 @@ describe EventsHelper do
end
it 'does not escape HTML entities' do
- expect(helper.event_commit_title("foo & bar")).to eq("foo & bar")
+ expect(helper.event_commit_title('foo & bar')).to eq('foo & bar')
end
end
@@ -30,38 +30,54 @@ describe EventsHelper do
let(:event) { create(:event) }
let(:project) { create(:project, :public, :repository) }
- it "returns project issue url" do
- event.target = create(:issue)
+ context 'issue' do
+ before do
+ event.target = create(:issue)
+ end
- expect(helper.event_feed_url(event)).to eq(project_issue_url(event.project, event.issue))
+ it 'returns the project issue url' do
+ expect(helper.event_feed_url(event)).to eq(project_issue_url(event.project, event.target))
+ end
+
+ it 'contains the project issue IID link' do
+ expect(helper.event_feed_title(event)).to include("##{event.target.iid}")
+ end
end
- it "returns project merge_request url" do
- event.target = create(:merge_request)
+ context 'merge request' do
+ before do
+ event.target = create(:merge_request)
+ end
+
+ it 'returns the project merge request url' do
+ expect(helper.event_feed_url(event)).to eq(project_merge_request_url(event.project, event.target))
+ end
- expect(helper.event_feed_url(event)).to eq(project_merge_request_url(event.project, event.merge_request))
+ it 'contains the project merge request IID link' do
+ expect(helper.event_feed_title(event)).to include("!#{event.target.iid}")
+ end
end
- it "returns project commit url" do
+ it 'returns project commit url' do
event.target = create(:note_on_commit, project: project)
expect(helper.event_feed_url(event)).to eq(project_commit_url(event.project, event.note_target))
end
- it "returns event note target url" do
+ it 'returns event note target url' do
event.target = create(:note)
expect(helper.event_feed_url(event)).to eq(event_note_target_url(event))
end
- it "returns project url" do
+ it 'returns project url' do
event.project = project
event.action = 1
expect(helper.event_feed_url(event)).to eq(project_url(event.project))
end
- it "returns push event feed url" do
+ it 'returns push event feed url' do
event = create(:push_event)
create(:push_event_payload, event: event, action: :pushed)
diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb
index a2cda58e5d2..c04f679bcf0 100644
--- a/spec/helpers/labels_helper_spec.rb
+++ b/spec/helpers/labels_helper_spec.rb
@@ -211,4 +211,29 @@ describe LabelsHelper do
end
end
end
+
+ describe 'labels_filter_path' do
+ let(:group) { create(:group) }
+ let(:project) { create(:project) }
+
+ it 'links to the dashboard labels page' do
+ expect(labels_filter_path).to eq(dashboard_labels_path)
+ end
+
+ it 'links to the group labels page' do
+ assign(:group, group)
+
+ expect(helper.labels_filter_path).to eq(group_labels_path(group))
+ end
+
+ it 'links to the project labels page' do
+ assign(:project, project)
+
+ expect(helper.labels_filter_path).to eq(project_labels_path(project))
+ end
+
+ it 'supports json format' do
+ expect(labels_filter_path(format: :json)).to eq(dashboard_labels_path(format: :json))
+ end
+ end
end
diff --git a/spec/helpers/profiles_helper_spec.rb b/spec/helpers/profiles_helper_spec.rb
index c1d0614c79e..8e336469c27 100644
--- a/spec/helpers/profiles_helper_spec.rb
+++ b/spec/helpers/profiles_helper_spec.rb
@@ -1,6 +1,40 @@
require 'rails_helper'
describe ProfilesHelper do
+ describe '#commit_email_select_options' do
+ it 'returns an array with private commit email along with all the verified emails' do
+ user = create(:user)
+ create(:email, user: user)
+ confirmed_email1 = create(:email, :confirmed, user: user)
+ confirmed_email2 = create(:email, :confirmed, user: user)
+
+ private_email = user.private_commit_email
+
+ emails = [
+ ["Use a private email - #{private_email}", Gitlab::PrivateCommitEmail::TOKEN],
+ user.email,
+ confirmed_email1.email,
+ confirmed_email2.email
+ ]
+
+ expect(helper.commit_email_select_options(user)).to match_array(emails)
+ end
+ end
+
+ describe '#selected_commit_email' do
+ let(:user) { create(:user) }
+
+ it 'returns main email when commit email attribute is nil' do
+ expect(helper.selected_commit_email(user)).to eq(user.email)
+ end
+
+ it 'returns DB stored commit_email' do
+ user.update(commit_email: Gitlab::PrivateCommitEmail::TOKEN)
+
+ expect(helper.selected_commit_email(user)).to eq(Gitlab::PrivateCommitEmail::TOKEN)
+ end
+ end
+
describe '#email_provider_label' do
it "returns nil for users without external email" do
user = create(:user)
diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb
index ffdf6561a53..ab4566e261b 100644
--- a/spec/helpers/tree_helper_spec.rb
+++ b/spec/helpers/tree_helper_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe TreeHelper do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
- let(:sha) { 'ce369011c189f62c815f5971d096b26759bab0d1' }
+ let(:sha) { 'c1c67abbaf91f624347bb3ae96eabe3a1b742478' }
describe '.render_tree' do
before do
@@ -32,6 +32,49 @@ describe TreeHelper do
end
end
+ describe '.fast_project_blob_path' do
+ it 'generates the same path as project_blob_path' do
+ blob_path = repository.tree(sha, 'with space').entries.first.path
+ fast_path = fast_project_blob_path(project, blob_path)
+ std_path = project_blob_path(project, blob_path)
+
+ expect(fast_path).to eq(std_path)
+ end
+
+ it 'generates the same path with encoded file names' do
+ tree = repository.tree(sha, 'encoding')
+ blob_path = tree.entries.find { |entry| entry.path == 'encoding/テスト.txt' }.path
+ fast_path = fast_project_blob_path(project, blob_path)
+ std_path = project_blob_path(project, blob_path)
+
+ expect(fast_path).to eq(std_path)
+ end
+
+ it 'respects a configured relative URL' do
+ allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return('/gitlab/root')
+ blob_path = repository.tree(sha, '').entries.first.path
+ fast_path = fast_project_blob_path(project, blob_path)
+
+ expect(fast_path).to start_with('/gitlab/root')
+ end
+ end
+
+ describe '.fast_project_tree_path' do
+ let(:tree_path) { repository.tree(sha, 'with space').path }
+ let(:fast_path) { fast_project_tree_path(project, tree_path) }
+ let(:std_path) { project_tree_path(project, tree_path) }
+
+ it 'generates the same path as project_tree_path' do
+ expect(fast_path).to eq(std_path)
+ end
+
+ it 'respects a configured relative URL' do
+ allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return('/gitlab/root')
+
+ expect(fast_path).to start_with('/gitlab/root')
+ end
+ end
+
describe 'flatten_tree' do
let(:tree) { repository.tree(sha, 'files') }
let(:root_path) { 'files' }
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index b0689fc7cfe..ce5d2022441 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -219,7 +219,7 @@ describe('AwardsHandler', function() {
expect($thumbsUpEmoji.data('originalTitle')).toBe('You, sam, jerry, max, and andy');
});
- it('handles the special case where "You" is not cleanly comma seperated', function() {
+ it('handles the special case where "You" is not cleanly comma separated', function() {
const awardUrl = awardsHandler.getAwardUrl();
const $votesBlock = $('.js-awards-block').eq(0);
const $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
@@ -244,7 +244,7 @@ describe('AwardsHandler', function() {
expect($thumbsUpEmoji.data('originalTitle')).toBe('sam, jerry, max, and andy');
});
- it('handles the special case where "You" is not cleanly comma seperated', function() {
+ it('handles the special case where "You" is not cleanly comma separated', function() {
const awardUrl = awardsHandler.getAwardUrl();
const $votesBlock = $('.js-awards-block').eq(0);
const $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js
index 037e06cf3b2..2642c8b1bdb 100644
--- a/spec/javascripts/boards/board_list_spec.js
+++ b/spec/javascripts/boards/board_list_spec.js
@@ -18,7 +18,7 @@ describe('Board list component', () => {
let mock;
let component;
- beforeEach((done) => {
+ beforeEach(done => {
const el = document.createElement('div');
document.body.appendChild(el);
@@ -62,122 +62,102 @@ describe('Board list component', () => {
});
it('renders component', () => {
- expect(
- component.$el.classList.contains('board-list-component'),
- ).toBe(true);
+ expect(component.$el.classList.contains('board-list-component')).toBe(true);
});
- it('renders loading icon', (done) => {
+ it('renders loading icon', done => {
component.loading = true;
Vue.nextTick(() => {
- expect(
- component.$el.querySelector('.board-list-loading'),
- ).not.toBeNull();
+ expect(component.$el.querySelector('.board-list-loading')).not.toBeNull();
done();
});
});
it('renders issues', () => {
- expect(
- component.$el.querySelectorAll('.board-card').length,
- ).toBe(1);
+ expect(component.$el.querySelectorAll('.board-card').length).toBe(1);
});
it('sets data attribute with issue id', () => {
- expect(
- component.$el.querySelector('.board-card').getAttribute('data-issue-id'),
- ).toBe('1');
+ expect(component.$el.querySelector('.board-card').getAttribute('data-issue-id')).toBe('1');
});
- it('shows new issue form', (done) => {
+ it('shows new issue form', done => {
component.toggleForm();
Vue.nextTick(() => {
- expect(
- component.$el.querySelector('.board-new-issue-form'),
- ).not.toBeNull();
+ expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull();
- expect(
- component.$el.querySelector('.is-smaller'),
- ).not.toBeNull();
+ expect(component.$el.querySelector('.is-smaller')).not.toBeNull();
done();
});
});
- it('shows new issue form after eventhub event', (done) => {
+ it('shows new issue form after eventhub event', done => {
eventHub.$emit(`hide-issue-form-${component.list.id}`);
Vue.nextTick(() => {
- expect(
- component.$el.querySelector('.board-new-issue-form'),
- ).not.toBeNull();
+ expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull();
- expect(
- component.$el.querySelector('.is-smaller'),
- ).not.toBeNull();
+ expect(component.$el.querySelector('.is-smaller')).not.toBeNull();
done();
});
});
- it('does not show new issue form for closed list', (done) => {
+ it('does not show new issue form for closed list', done => {
component.list.type = 'closed';
component.toggleForm();
Vue.nextTick(() => {
- expect(
- component.$el.querySelector('.board-new-issue-form'),
- ).toBeNull();
+ expect(component.$el.querySelector('.board-new-issue-form')).toBeNull();
done();
});
});
- it('shows count list item', (done) => {
+ it('shows count list item', done => {
component.showCount = true;
Vue.nextTick(() => {
- expect(
- component.$el.querySelector('.board-list-count'),
- ).not.toBeNull();
+ expect(component.$el.querySelector('.board-list-count')).not.toBeNull();
- expect(
- component.$el.querySelector('.board-list-count').textContent.trim(),
- ).toBe('Showing all issues');
+ expect(component.$el.querySelector('.board-list-count').textContent.trim()).toBe(
+ 'Showing all issues',
+ );
done();
});
});
- it('sets data attribute with invalid id', (done) => {
+ it('sets data attribute with invalid id', done => {
component.showCount = true;
Vue.nextTick(() => {
- expect(
- component.$el.querySelector('.board-list-count').getAttribute('data-issue-id'),
- ).toBe('-1');
+ expect(component.$el.querySelector('.board-list-count').getAttribute('data-issue-id')).toBe(
+ '-1',
+ );
done();
});
});
- it('shows how many more issues to load', (done) => {
+ it('shows how many more issues to load', done => {
component.showCount = true;
component.list.issuesSize = 20;
Vue.nextTick(() => {
- expect(
- component.$el.querySelector('.board-list-count').textContent.trim(),
- ).toBe('Showing 1 of 20 issues');
+ expect(component.$el.querySelector('.board-list-count').textContent.trim()).toBe(
+ 'Showing 1 of 20 issues',
+ );
done();
});
});
- it('loads more issues after scrolling', (done) => {
+ it('loads more issues after scrolling', done => {
spyOn(component.list, 'nextPage');
component.$refs.list.style.height = '100px';
component.$refs.list.style.overflow = 'scroll';
@@ -200,7 +180,9 @@ describe('Board list component', () => {
});
it('does not load issues if already loading', () => {
- component.list.nextPage = spyOn(component.list, 'nextPage').and.returnValue(new Promise(() => {}));
+ component.list.nextPage = spyOn(component.list, 'nextPage').and.returnValue(
+ new Promise(() => {}),
+ );
component.onScroll();
component.onScroll();
@@ -208,14 +190,12 @@ describe('Board list component', () => {
expect(component.list.nextPage).toHaveBeenCalledTimes(1);
});
- it('shows loading more spinner', (done) => {
+ it('shows loading more spinner', done => {
component.showCount = true;
component.list.loadingMore = true;
Vue.nextTick(() => {
- expect(
- component.$el.querySelector('.board-list-count .fa-spinner'),
- ).not.toBeNull();
+ expect(component.$el.querySelector('.board-list-count .fa-spinner')).not.toBeNull();
done();
});
diff --git a/spec/javascripts/boards/components/board_spec.js b/spec/javascripts/boards/components/board_spec.js
index d4c53bd5a7d..dee7841c088 100644
--- a/spec/javascripts/boards/components/board_spec.js
+++ b/spec/javascripts/boards/components/board_spec.js
@@ -8,7 +8,7 @@ describe('Board component', () => {
let vm;
let el;
- beforeEach((done) => {
+ beforeEach(done => {
loadFixtures('boards/show.html.raw');
el = document.createElement('div');
@@ -50,56 +50,46 @@ describe('Board component', () => {
});
it('board is expandable when list type is backlog', () => {
- expect(
- vm.$el.classList.contains('is-expandable'),
- ).toBe(true);
+ expect(vm.$el.classList.contains('is-expandable')).toBe(true);
});
- it('board is expandable when list type is closed', (done) => {
+ it('board is expandable when list type is closed', done => {
vm.list.type = 'closed';
Vue.nextTick(() => {
- expect(
- vm.$el.classList.contains('is-expandable'),
- ).toBe(true);
+ expect(vm.$el.classList.contains('is-expandable')).toBe(true);
done();
});
});
- it('board is not expandable when list type is label', (done) => {
+ it('board is not expandable when list type is label', done => {
vm.list.type = 'label';
vm.list.isExpandable = false;
Vue.nextTick(() => {
- expect(
- vm.$el.classList.contains('is-expandable'),
- ).toBe(false);
+ expect(vm.$el.classList.contains('is-expandable')).toBe(false);
done();
});
});
- it('collapses when clicking header', (done) => {
+ it('collapses when clicking header', done => {
vm.$el.querySelector('.board-header').click();
Vue.nextTick(() => {
- expect(
- vm.$el.classList.contains('is-collapsed'),
- ).toBe(true);
+ expect(vm.$el.classList.contains('is-collapsed')).toBe(true);
done();
});
});
- it('created sets isExpanded to true from localStorage', (done) => {
+ it('created sets isExpanded to true from localStorage', done => {
vm.$el.querySelector('.board-header').click();
return Vue.nextTick()
.then(() => {
- expect(
- vm.$el.classList.contains('is-collapsed'),
- ).toBe(true);
+ expect(vm.$el.classList.contains('is-collapsed')).toBe(true);
// call created manually
vm.$options.created[0].call(vm);
@@ -107,11 +97,10 @@ describe('Board component', () => {
return Vue.nextTick();
})
.then(() => {
- expect(
- vm.$el.classList.contains('is-collapsed'),
- ).toBe(true);
+ expect(vm.$el.classList.contains('is-collapsed')).toBe(true);
done();
- }).catch(done.fail);
+ })
+ .catch(done.fail);
});
});
diff --git a/spec/javascripts/boards/components/issue_due_date_spec.js b/spec/javascripts/boards/components/issue_due_date_spec.js
new file mode 100644
index 00000000000..9e49330c052
--- /dev/null
+++ b/spec/javascripts/boards/components/issue_due_date_spec.js
@@ -0,0 +1,64 @@
+import Vue from 'vue';
+import dateFormat from 'dateformat';
+import IssueDueDate from '~/boards/components/issue_due_date.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('Issue Due Date component', () => {
+ let vm;
+ let date;
+ const Component = Vue.extend(IssueDueDate);
+ const createComponent = (dueDate = new Date()) =>
+ mountComponent(Component, { date: dateFormat(dueDate, 'yyyy-mm-dd', true) });
+
+ beforeEach(() => {
+ date = new Date();
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render "Today" if the due date is today', () => {
+ const timeContainer = vm.$el.querySelector('time');
+
+ expect(timeContainer.textContent.trim()).toEqual('Today');
+ });
+
+ it('should render "Yesterday" if the due date is yesterday', () => {
+ date.setDate(date.getDate() - 1);
+ vm = createComponent(date);
+
+ expect(vm.$el.querySelector('time').textContent.trim()).toEqual('Yesterday');
+ });
+
+ it('should render "Tomorrow" if the due date is one day from now', () => {
+ date.setDate(date.getDate() + 1);
+ vm = createComponent(date);
+
+ expect(vm.$el.querySelector('time').textContent.trim()).toEqual('Tomorrow');
+ });
+
+ it('should render day of the week if due date is one week away', () => {
+ date.setDate(date.getDate() + 5);
+ vm = createComponent(date);
+
+ expect(vm.$el.querySelector('time').textContent.trim()).toEqual(dateFormat(date, 'dddd', true));
+ });
+
+ it('should render month and day for other dates', () => {
+ date.setDate(date.getDate() + 17);
+ vm = createComponent(date);
+
+ expect(vm.$el.querySelector('time').textContent.trim()).toEqual(
+ dateFormat(date, 'mmm d', true),
+ );
+ });
+
+ it('should contain the correct `.text-danger` css class for overdue issue', () => {
+ date.setDate(date.getDate() - 17);
+ vm = createComponent(date);
+
+ expect(vm.$el.querySelector('time').classList.contains('text-danger')).toEqual(true);
+ });
+});
diff --git a/spec/javascripts/boards/components/issue_time_estimate_spec.js b/spec/javascripts/boards/components/issue_time_estimate_spec.js
new file mode 100644
index 00000000000..ba65d3287da
--- /dev/null
+++ b/spec/javascripts/boards/components/issue_time_estimate_spec.js
@@ -0,0 +1,40 @@
+import Vue from 'vue';
+import IssueTimeEstimate from '~/boards/components/issue_time_estimate.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('Issue Tine Estimate component', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(IssueTimeEstimate);
+ vm = mountComponent(Component, {
+ estimate: 374460,
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders the correct time estimate', () => {
+ expect(vm.$el.querySelector('time').textContent.trim()).toEqual('2w 3d 1m');
+ });
+
+ it('renders expanded time estimate in tooltip', () => {
+ expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain(
+ '2 weeks 3 days 1 minute',
+ );
+ });
+
+ it('prevents tooltip xss', done => {
+ const alertSpy = spyOn(window, 'alert');
+ vm.estimate = 'Foo <script>alert("XSS")</script>';
+
+ vm.$nextTick(() => {
+ expect(alertSpy).not.toHaveBeenCalled();
+ expect(vm.$el.querySelector('time').textContent.trim()).toEqual('0m');
+ expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain('0m');
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js
index 58b7d45d913..6eda5047dd0 100644
--- a/spec/javascripts/boards/issue_card_spec.js
+++ b/spec/javascripts/boards/issue_card_spec.js
@@ -117,11 +117,9 @@ describe('Issue card component', () => {
});
it('sets title', () => {
- expect(
- component.$el
- .querySelector('.board-card-assignee img')
- .getAttribute('data-original-title'),
- ).toContain(`Assigned to ${user.name}`);
+ expect(component.$el.querySelector('.js-assignee-tooltip').textContent).toContain(
+ `${user.name}`,
+ );
});
it('sets users path', () => {
@@ -154,7 +152,7 @@ describe('Issue card component', () => {
it('displays defaults avatar if users avatar is null', () => {
expect(component.$el.querySelector('.board-card-assignee img')).not.toBeNull();
expect(component.$el.querySelector('.board-card-assignee img').getAttribute('src')).toBe(
- 'default_avatar?width=20',
+ 'default_avatar?width=24',
);
});
});
@@ -163,7 +161,6 @@ describe('Issue card component', () => {
describe('multiple assignees', () => {
beforeEach(done => {
component.issue.assignees = [
- user,
new ListAssignee({
id: 2,
name: 'user2',
@@ -187,11 +184,11 @@ describe('Issue card component', () => {
Vue.nextTick(() => done());
});
- it('renders all four assignees', () => {
- expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(4);
+ it('renders all three assignees', () => {
+ expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(3);
});
- describe('more than four assignees', () => {
+ describe('more than three assignees', () => {
beforeEach(done => {
component.issue.assignees.push(
new ListAssignee({
@@ -207,12 +204,12 @@ describe('Issue card component', () => {
it('renders more avatar counter', () => {
expect(
- component.$el.querySelector('.board-card-assignee .avatar-counter').innerText,
+ component.$el.querySelector('.board-card-assignee .avatar-counter').innerText.trim(),
).toEqual('+2');
});
- it('renders three assignees', () => {
- expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(3);
+ it('renders two assignees', () => {
+ expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(2);
});
it('renders 99+ avatar counter', done => {
@@ -228,7 +225,7 @@ describe('Issue card component', () => {
Vue.nextTick(() => {
expect(
- component.$el.querySelector('.board-card-assignee .avatar-counter').innerText,
+ component.$el.querySelector('.board-card-assignee .avatar-counter').innerText.trim(),
).toEqual('99+');
done();
});
diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/javascripts/clusters/components/applications_spec.js
index a70138c7eee..0e2cc13fa52 100644
--- a/spec/javascripts/clusters/components/applications_spec.js
+++ b/spec/javascripts/clusters/components/applications_spec.js
@@ -23,6 +23,7 @@ describe('Applications', () => {
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub' },
+ knative: { title: 'Knative' },
},
});
});
@@ -46,6 +47,10 @@ describe('Applications', () => {
it('renders a row for Jupyter', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-jupyter')).not.toBe(null);
});
+
+ it('renders a row for Knative', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-knative')).not.toBe(null);
+ });
});
describe('Ingress application', () => {
@@ -63,6 +68,7 @@ describe('Applications', () => {
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', hostname: '' },
+ knative: { title: 'Knative', hostname: '' },
},
});
@@ -86,6 +92,7 @@ describe('Applications', () => {
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', hostname: '' },
+ knative: { title: 'Knative', hostname: '' },
},
});
@@ -105,6 +112,7 @@ describe('Applications', () => {
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', hostname: '' },
+ knative: { title: 'Knative', hostname: '' },
},
});
@@ -123,6 +131,7 @@ describe('Applications', () => {
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' },
+ knative: { title: 'Knative', hostname: '', status: 'installable' },
},
});
@@ -139,6 +148,7 @@ describe('Applications', () => {
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' },
+ knative: { title: 'Knative', hostname: '', status: 'installable' },
},
});
@@ -155,6 +165,7 @@ describe('Applications', () => {
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', status: 'installed', hostname: '' },
+ knative: { title: 'Knative', status: 'installed', hostname: '' },
},
});
@@ -171,6 +182,7 @@ describe('Applications', () => {
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', status: 'not_installable' },
+ knative: { title: 'Knative' },
},
});
});
diff --git a/spec/javascripts/clusters/services/mock_data.js b/spec/javascripts/clusters/services/mock_data.js
index 4e6ad11cd92..73abf6504c0 100644
--- a/spec/javascripts/clusters/services/mock_data.js
+++ b/spec/javascripts/clusters/services/mock_data.js
@@ -33,6 +33,11 @@ const CLUSTERS_MOCK_DATA = {
status: APPLICATION_STATUS.INSTALLING,
status_reason: 'Cannot connect',
},
+ {
+ name: 'knative',
+ status: APPLICATION_STATUS.INSTALLING,
+ status_reason: 'Cannot connect',
+ },
],
},
},
@@ -67,6 +72,11 @@ const CLUSTERS_MOCK_DATA = {
status: APPLICATION_STATUS.INSTALLABLE,
status_reason: 'Cannot connect',
},
+ {
+ name: 'knative',
+ status: APPLICATION_STATUS.INSTALLABLE,
+ status_reason: 'Cannot connect',
+ },
],
},
},
@@ -77,6 +87,7 @@ const CLUSTERS_MOCK_DATA = {
'/gitlab-org/gitlab-shell/clusters/1/applications/runner': {},
'/gitlab-org/gitlab-shell/clusters/1/applications/prometheus': {},
'/gitlab-org/gitlab-shell/clusters/1/applications/jupyter': {},
+ '/gitlab-org/gitlab-shell/clusters/1/applications/knative': {},
},
};
diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/javascripts/clusters/stores/clusters_store_spec.js
index e0f55a12fca..34ed36afa5b 100644
--- a/spec/javascripts/clusters/stores/clusters_store_spec.js
+++ b/spec/javascripts/clusters/stores/clusters_store_spec.js
@@ -100,6 +100,14 @@ describe('Clusters Store', () => {
requestReason: null,
hostname: '',
},
+ knative: {
+ title: 'Knative',
+ status: mockResponseData.applications[5].status,
+ statusReason: mockResponseData.applications[5].status_reason,
+ requestStatus: null,
+ requestReason: null,
+ hostname: null,
+ },
},
});
});
diff --git a/spec/javascripts/commit/commit_pipeline_status_component_spec.js b/spec/javascripts/commit/commit_pipeline_status_component_spec.js
index 4fc56fd9a27..f6b36e88a5f 100644
--- a/spec/javascripts/commit/commit_pipeline_status_component_spec.js
+++ b/spec/javascripts/commit/commit_pipeline_status_component_spec.js
@@ -22,7 +22,7 @@ describe('Commit pipeline status component', () => {
Component = Vue.extend(commitPipelineStatus);
});
- describe('While polling pipeline data succesfully', () => {
+ describe('While polling pipeline data successfully', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet('/dummy/endpoint').reply(() => {
@@ -59,14 +59,14 @@ describe('Commit pipeline status component', () => {
});
});
- it('contains a ciStatus when the polling is succesful ', done => {
+ it('contains a ciStatus when the polling is successful ', done => {
setTimeout(() => {
expect(vm.ciStatus).toEqual(mockCiStatus);
done();
});
});
- it('contains a ci-status icon when polling is succesful', done => {
+ it('contains a ci-status icon when polling is successful', done => {
setTimeout(() => {
expect(vm.$el.querySelector('.ci-status-icon')).not.toBe(null);
expect(vm.$el.querySelector('.ci-status-icon').classList).toContain(
@@ -77,7 +77,7 @@ describe('Commit pipeline status component', () => {
});
});
- describe('When polling data was not succesful', () => {
+ describe('When polling data was not successful', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet('/dummy/endpoint').reply(502, {});
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js
index b797cc44ae7..04c8ab44405 100644
--- a/spec/javascripts/commit/pipelines/pipelines_spec.js
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js
@@ -72,6 +72,29 @@ describe('Pipelines table in Commits and Merge requests', function() {
done();
}, 0);
});
+
+ describe('with pagination', () => {
+ it('should make an API request when using pagination', done => {
+ setTimeout(() => {
+ spyOn(vm, 'updateContent');
+
+ vm.store.state.pageInfo = {
+ page: 1,
+ total: 10,
+ perPage: 2,
+ nextPage: 2,
+ totalPages: 5,
+ };
+
+ vm.$nextTick(() => {
+ vm.$el.querySelector('.js-next-button a').click();
+
+ expect(vm.updateContent).toHaveBeenCalledWith({ page: '2' });
+ done();
+ });
+ });
+ });
+ });
});
describe('pipeline badge counts', () => {
diff --git a/spec/javascripts/diffs/components/commit_item_spec.js b/spec/javascripts/diffs/components/commit_item_spec.js
index 7606847ada9..8b2ca6506c4 100644
--- a/spec/javascripts/diffs/components/commit_item_spec.js
+++ b/spec/javascripts/diffs/components/commit_item_spec.js
@@ -21,7 +21,7 @@ const getAvatarElement = vm => vm.$el.querySelector('.user-avatar-link');
const getCommitterElement = vm => vm.$el.querySelector('.commiter');
const getCommitActionsElement = vm => vm.$el.querySelector('.commit-actions');
-describe('diffs/components/commit_widget', () => {
+describe('diffs/components/commit_item', () => {
const Component = Vue.extend(CommitItem);
const timeago = getTimeago();
const { commit } = getDiffWithCommit();
@@ -37,15 +37,15 @@ describe('diffs/components/commit_widget', () => {
it('renders commit title', () => {
const titleElement = getTitleElement(vm);
- expect(titleElement).toHaveAttr('href', commit.commitUrl);
- expect(titleElement).toHaveText(commit.titleHtml);
+ expect(titleElement).toHaveAttr('href', commit.commit_url);
+ expect(titleElement).toHaveText(commit.title_html);
});
it('renders commit description', () => {
const descElement = getDescElement(vm);
const descExpandElement = getDescExpandElement(vm);
- const expected = commit.descriptionHtml.replace(/&#x000A;/g, '');
+ const expected = commit.description_html.replace(/&#x000A;/g, '');
expect(trimText(descElement.innerHTML)).toEqual(trimText(expected));
expect(descExpandElement).not.toBeNull();
@@ -56,7 +56,7 @@ describe('diffs/components/commit_widget', () => {
const labelElement = shaElement.querySelector('.label');
const buttonElement = shaElement.querySelector('button');
- expect(labelElement.textContent).toEqual(commit.shortId);
+ expect(labelElement.textContent).toEqual(commit.short_id);
expect(buttonElement).toHaveData('clipboard-text', commit.id);
});
@@ -64,27 +64,27 @@ describe('diffs/components/commit_widget', () => {
const avatarElement = getAvatarElement(vm);
const imgElement = avatarElement.querySelector('img');
- expect(avatarElement).toHaveAttr('href', commit.author.webUrl);
+ expect(avatarElement).toHaveAttr('href', commit.author.web_url);
expect(imgElement).toHaveClass('s36');
expect(imgElement).toHaveAttr('alt', commit.author.name);
- expect(imgElement).toHaveAttr('src', commit.author.avatarUrl);
+ expect(imgElement).toHaveAttr('src', commit.author.avatar_url);
});
it('renders committer text', () => {
const committerElement = getCommitterElement(vm);
const nameElement = committerElement.querySelector('a');
- const expectTimeText = timeago.format(commit.authoredDate);
+ const expectTimeText = timeago.format(commit.authored_date);
const expectedText = `${commit.author.name} authored ${expectTimeText}`;
expect(trimText(committerElement.textContent)).toEqual(expectedText);
- expect(nameElement).toHaveAttr('href', commit.author.webUrl);
+ expect(nameElement).toHaveAttr('href', commit.author.web_url);
expect(nameElement).toHaveText(commit.author.name);
});
describe('without commit description', () => {
beforeEach(done => {
- vm.commit.descriptionHtml = '';
+ vm.commit.description_html = '';
vm.$nextTick()
.then(done)
@@ -103,9 +103,9 @@ describe('diffs/components/commit_widget', () => {
describe('with no matching user', () => {
beforeEach(done => {
vm.commit.author = null;
- vm.commit.authorEmail = TEST_AUTHOR_EMAIL;
- vm.commit.authorName = TEST_AUTHOR_NAME;
- vm.commit.authorGravatarUrl = TEST_AUTHOR_GRAVATAR;
+ vm.commit.author_email = TEST_AUTHOR_EMAIL;
+ vm.commit.author_name = TEST_AUTHOR_NAME;
+ vm.commit.author_gravatar_url = TEST_AUTHOR_GRAVATAR;
vm.$nextTick()
.then(done)
@@ -132,7 +132,7 @@ describe('diffs/components/commit_widget', () => {
describe('with signature', () => {
beforeEach(done => {
- vm.commit.signatureHtml = TEST_SIGNATURE_HTML;
+ vm.commit.signature_html = TEST_SIGNATURE_HTML;
vm.$nextTick()
.then(done)
@@ -148,7 +148,7 @@ describe('diffs/components/commit_widget', () => {
describe('with pipeline status', () => {
beforeEach(done => {
- vm.commit.pipelineStatusPath = TEST_PIPELINE_STATUS_PATH;
+ vm.commit.pipeline_status_path = TEST_PIPELINE_STATUS_PATH;
vm.$nextTick()
.then(done)
diff --git a/spec/javascripts/diffs/components/commit_widget_spec.js b/spec/javascripts/diffs/components/commit_widget_spec.js
index 951eb57255d..2b60bd232ed 100644
--- a/spec/javascripts/diffs/components/commit_widget_spec.js
+++ b/spec/javascripts/diffs/components/commit_widget_spec.js
@@ -19,6 +19,6 @@ describe('diffs/components/commit_widget', () => {
const commitElement = vm.$el.querySelector('li.commit');
expect(commitElement).not.toBeNull();
- expect(commitElement).toContainText(commit.shortId);
+ expect(commitElement).toContainText(commit.short_id);
});
});
diff --git a/spec/javascripts/diffs/components/diff_content_spec.js b/spec/javascripts/diffs/components/diff_content_spec.js
index 67f7b569f47..c25f6167163 100644
--- a/spec/javascripts/diffs/components/diff_content_spec.js
+++ b/spec/javascripts/diffs/components/diff_content_spec.js
@@ -1,15 +1,24 @@
import Vue from 'vue';
import DiffContentComponent from '~/diffs/components/diff_content.vue';
-import store from '~/mr_notes/stores';
+import { createStore } from '~/mr_notes/stores';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants';
+import '~/behaviors/markdown/render_gfm';
import diffFileMockData from '../mock_data/diff_file';
+import discussionsMockData from '../mock_data/diff_discussions';
describe('DiffContent', () => {
const Component = Vue.extend(DiffContentComponent);
let vm;
beforeEach(() => {
+ const store = createStore();
+ store.state.notes.noteableData = {
+ current_user: {
+ can_create_note: false,
+ },
+ };
+
vm = mountComponentWithStore(Component, {
store,
props: {
@@ -46,21 +55,57 @@ describe('DiffContent', () => {
});
describe('image diff', () => {
- beforeEach(() => {
- vm.diffFile.newPath = GREEN_BOX_IMAGE_URL;
- vm.diffFile.newSha = 'DEF';
- vm.diffFile.oldPath = RED_BOX_IMAGE_URL;
- vm.diffFile.oldSha = 'ABC';
- vm.diffFile.viewPath = '';
+ beforeEach(done => {
+ vm.diffFile.new_path = GREEN_BOX_IMAGE_URL;
+ vm.diffFile.new_sha = 'DEF';
+ vm.diffFile.old_path = RED_BOX_IMAGE_URL;
+ vm.diffFile.old_sha = 'ABC';
+ vm.diffFile.view_path = '';
+ vm.diffFile.discussions = [{ ...discussionsMockData }];
+ vm.$store.state.diffs.commentForms.push({
+ fileHash: vm.diffFile.file_hash,
+ x: 10,
+ y: 20,
+ width: 100,
+ height: 200,
+ });
+
+ vm.$nextTick(done);
});
- it('should have image diff view in place', done => {
- vm.$nextTick(() => {
- expect(vm.$el.querySelectorAll('.js-diff-inline-view').length).toEqual(0);
+ it('should have image diff view in place', () => {
+ expect(vm.$el.querySelectorAll('.js-diff-inline-view').length).toEqual(0);
- expect(vm.$el.querySelectorAll('.diff-viewer .image').length).toEqual(1);
+ expect(vm.$el.querySelectorAll('.diff-viewer .image').length).toEqual(1);
+ });
- done();
+ it('renders image diff overlay', () => {
+ expect(vm.$el.querySelector('.image-diff-overlay')).not.toBe(null);
+ });
+
+ it('renders diff file discussions', () => {
+ expect(vm.$el.querySelectorAll('.discussion .note.timeline-entry').length).toEqual(5);
+ });
+
+ describe('handleSaveNote', () => {
+ it('dispatches handleSaveNote', () => {
+ spyOn(vm.$store, 'dispatch').and.stub();
+
+ vm.handleSaveNote('test');
+
+ expect(vm.$store.dispatch).toHaveBeenCalledWith('diffs/saveDiffDiscussion', {
+ note: 'test',
+ formData: {
+ noteableData: jasmine.anything(),
+ noteableType: jasmine.anything(),
+ diffFile: vm.diffFile,
+ positionType: 'image',
+ x: 10,
+ y: 20,
+ width: 100,
+ height: 200,
+ },
+ });
});
});
});
@@ -68,10 +113,10 @@ describe('DiffContent', () => {
describe('file diff', () => {
it('should have download buttons in place', done => {
const el = vm.$el;
- vm.diffFile.newPath = 'test.abc';
- vm.diffFile.newSha = 'DEF';
- vm.diffFile.oldPath = 'test.abc';
- vm.diffFile.oldSha = 'ABC';
+ vm.diffFile.new_path = 'test.abc';
+ vm.diffFile.new_sha = 'DEF';
+ vm.diffFile.old_path = 'test.abc';
+ vm.diffFile.old_sha = 'ABC';
vm.$nextTick(() => {
expect(el.querySelectorAll('.js-diff-inline-view').length).toEqual(0);
diff --git a/spec/javascripts/diffs/components/diff_discussions_spec.js b/spec/javascripts/diffs/components/diff_discussions_spec.js
index 270f363825f..0bc9da5ad0f 100644
--- a/spec/javascripts/diffs/components/diff_discussions_spec.js
+++ b/spec/javascripts/diffs/components/diff_discussions_spec.js
@@ -1,24 +1,90 @@
import Vue from 'vue';
import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
-import store from '~/mr_notes/stores';
+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';
describe('DiffDiscussions', () => {
- let component;
+ let vm;
const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)];
- beforeEach(() => {
- component = createComponentWithStore(Vue.extend(DiffDiscussions), store, {
+ function createComponent(props = {}) {
+ const store = createStore();
+
+ vm = createComponentWithStore(Vue.extend(DiffDiscussions), store, {
discussions: getDiscussionsMockData(),
+ ...props,
}).$mount();
+ }
+
+ afterEach(() => {
+ vm.$destroy();
});
describe('template', () => {
it('should have notes list', () => {
- const { $el } = component;
+ createComponent();
+
+ expect(vm.$el.querySelectorAll('.discussion .note.timeline-entry').length).toEqual(5);
+ });
+ });
+
+ describe('image commenting', () => {
+ it('renders collapsible discussion button', () => {
+ createComponent({ shouldCollapseDiscussions: true });
+
+ 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',
+ );
+ });
+
+ it('dispatches toggleDiscussion when clicking collapse button', () => {
+ createComponent({ shouldCollapseDiscussions: true });
+
+ 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,
+ });
+ });
+
+ 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',
+ );
+
+ done();
+ });
+ });
+
+ it('hides discussion when collapsed', done => {
+ createComponent({ shouldCollapseDiscussions: true });
+
+ vm.discussions[0].expanded = false;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.note-discussion').style.display).toBe('none');
+
+ done();
+ });
+ });
+
+ it('renders badge on avatar', () => {
+ createComponent({ renderAvatarBadge: true, discussions: [{ ...discussionsMockData }] });
- expect($el.querySelectorAll('.discussion .note.timeline-entry').length).toEqual(5);
+ 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');
});
});
});
diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js
index 0192d583c6c..9530b50c729 100644
--- a/spec/javascripts/diffs/components/diff_file_header_spec.js
+++ b/spec/javascripts/diffs/components/diff_file_header_spec.js
@@ -3,7 +3,6 @@ import Vuex from 'vuex';
import diffsModule from '~/diffs/store/modules';
import notesModule from '~/notes/stores/modules';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
Vue.use(Vuex);
@@ -24,9 +23,9 @@ describe('diff_file_header', () => {
});
beforeEach(() => {
- const diffFile = convertObjectPropsToCamelCase(diffDiscussionMock.diff_file, { deep: true });
+ const diffFile = diffDiscussionMock.diff_file;
props = {
- diffFile,
+ diffFile: { ...diffFile },
canCurrentUserFork: false,
};
});
@@ -62,8 +61,8 @@ describe('diff_file_header', () => {
beforeEach(() => {
props.discussionPath = 'link://to/discussion';
Object.assign(props.diffFile, {
- submoduleLink: 'link://to/submodule',
- submoduleTreeUrl: 'some://tree/url',
+ submodule_link: 'link://to/submodule',
+ submodule_tree_url: 'some://tree/url',
});
});
@@ -80,18 +79,18 @@ describe('diff_file_header', () => {
vm = mountComponentWithStore(Component, { props, store });
- expect(vm.titleLink).toBe(props.diffFile.submoduleTreeUrl);
+ expect(vm.titleLink).toBe(props.diffFile.submodule_tree_url);
});
it('returns the submoduleLink for submodules without submoduleTreeUrl', () => {
Object.assign(props.diffFile, {
submodule: true,
- submoduleTreeUrl: null,
+ submodule_tree_url: null,
});
vm = mountComponentWithStore(Component, { props, store });
- expect(vm.titleLink).toBe(props.diffFile.submoduleLink);
+ expect(vm.titleLink).toBe(props.diffFile.submodule_link);
});
it('sets the correct path to the discussion', () => {
@@ -107,7 +106,7 @@ describe('diff_file_header', () => {
beforeEach(() => {
Object.assign(props.diffFile, {
blob: { id: 'b10b1db10b1d' },
- filePath: 'path/to/file',
+ file_path: 'path/to/file',
});
});
@@ -116,7 +115,7 @@ describe('diff_file_header', () => {
vm = mountComponentWithStore(Component, { props, store });
- expect(vm.filePath).toBe(props.diffFile.filePath);
+ expect(vm.filePath).toBe(props.diffFile.file_path);
});
it('appends the truncated blob id for submodules', () => {
@@ -125,14 +124,14 @@ describe('diff_file_header', () => {
vm = mountComponentWithStore(Component, { props, store });
expect(vm.filePath).toBe(
- `${props.diffFile.filePath} @ ${props.diffFile.blob.id.substr(0, 8)}`,
+ `${props.diffFile.file_path} @ ${props.diffFile.blob.id.substr(0, 8)}`,
);
});
});
describe('titleTag', () => {
it('returns a link tag if fileHash is set', () => {
- props.diffFile.fileHash = 'some hash';
+ props.diffFile.file_hash = 'some hash';
vm = mountComponentWithStore(Component, { props, store });
@@ -140,7 +139,7 @@ describe('diff_file_header', () => {
});
it('returns a span tag if fileHash is not set', () => {
- props.diffFile.fileHash = null;
+ props.diffFile.file_hash = null;
vm = mountComponentWithStore(Component, { props, store });
@@ -151,8 +150,8 @@ describe('diff_file_header', () => {
describe('isUsingLfs', () => {
beforeEach(() => {
Object.assign(props.diffFile, {
- storedExternally: true,
- externalStorage: 'lfs',
+ stored_externally: true,
+ external_storage: 'lfs',
});
});
@@ -163,7 +162,7 @@ describe('diff_file_header', () => {
});
it('returns false if file is not stored externally', () => {
- props.diffFile.storedExternally = false;
+ props.diffFile.stored_externally = false;
vm = mountComponentWithStore(Component, { props, store });
@@ -171,7 +170,7 @@ describe('diff_file_header', () => {
});
it('returns false if file is not stored in LFS', () => {
- props.diffFile.externalStorage = 'not lfs';
+ props.diffFile.external_storage = 'not lfs';
vm = mountComponentWithStore(Component, { props, store });
@@ -200,7 +199,7 @@ describe('diff_file_header', () => {
describe('viewFileButtonText', () => {
it('contains the truncated content SHA', () => {
const dummySha = 'deebd00f is no SHA';
- props.diffFile.contentSha = dummySha;
+ props.diffFile.content_sha = dummySha;
vm = mountComponentWithStore(Component, { props, store });
@@ -212,7 +211,7 @@ describe('diff_file_header', () => {
describe('viewReplacedFileButtonText', () => {
it('contains the truncated base SHA', () => {
const dummySha = 'deadabba sings no more';
- props.diffFile.diffRefs.baseSha = dummySha;
+ props.diffFile.diff_refs.base_sha = dummySha;
vm = mountComponentWithStore(Component, { props, store });
@@ -281,32 +280,32 @@ describe('diff_file_header', () => {
const filePaths = () => vm.$el.querySelectorAll('.file-title-name');
it('displays the path of a added file', () => {
- props.diffFile.renamedFile = false;
+ props.diffFile.renamed_file = false;
vm = mountComponentWithStore(Component, { props, store });
expect(filePaths()).toHaveLength(1);
- expect(filePaths()[0]).toHaveText(props.diffFile.filePath);
+ expect(filePaths()[0]).toHaveText(props.diffFile.file_path);
});
it('displays path for deleted file', () => {
- props.diffFile.renamedFile = false;
- props.diffFile.deletedFile = true;
+ props.diffFile.renamed_file = false;
+ props.diffFile.deleted_file = true;
vm = mountComponentWithStore(Component, { props, store });
expect(filePaths()).toHaveLength(1);
- expect(filePaths()[0]).toHaveText(`${props.diffFile.filePath} deleted`);
+ expect(filePaths()[0]).toHaveText(`${props.diffFile.file_path} deleted`);
});
it('displays old and new path if the file was renamed', () => {
- props.diffFile.renamedFile = true;
+ props.diffFile.renamed_file = true;
vm = mountComponentWithStore(Component, { props, store });
expect(filePaths()).toHaveLength(2);
- expect(filePaths()[0]).toHaveText(props.diffFile.oldPath);
- expect(filePaths()[1]).toHaveText(props.diffFile.newPath);
+ expect(filePaths()[0]).toHaveText(props.diffFile.old_path);
+ expect(filePaths()[1]).toHaveText(props.diffFile.new_path);
});
});
@@ -323,19 +322,19 @@ describe('diff_file_header', () => {
describe('file mode', () => {
it('it displays old and new file mode if it changed', () => {
- props.diffFile.modeChanged = true;
+ props.diffFile.mode_changed = true;
vm = mountComponentWithStore(Component, { props, store });
const { fileMode } = vm.$refs;
expect(fileMode).not.toBe(undefined);
- expect(fileMode).toContainText(props.diffFile.aMode);
- expect(fileMode).toContainText(props.diffFile.bMode);
+ expect(fileMode).toContainText(props.diffFile.a_mode);
+ expect(fileMode).toContainText(props.diffFile.b_mode);
});
it('does not display the file mode if it has not changed', () => {
- props.diffFile.modeChanged = false;
+ props.diffFile.mode_changed = false;
vm = mountComponentWithStore(Component, { props, store });
@@ -350,8 +349,8 @@ describe('diff_file_header', () => {
it('displays the LFS label for files stored in LFS', () => {
Object.assign(props.diffFile, {
- storedExternally: true,
- externalStorage: 'lfs',
+ stored_externally: true,
+ external_storage: 'lfs',
});
vm = mountComponentWithStore(Component, { props, store });
@@ -361,7 +360,7 @@ describe('diff_file_header', () => {
});
it('does not display the LFS label for files stored in repository', () => {
- props.diffFile.storedExternally = false;
+ props.diffFile.stored_externally = false;
vm = mountComponentWithStore(Component, { props, store });
@@ -378,7 +377,7 @@ describe('diff_file_header', () => {
it('should show edit button when file is editable', () => {
props.addMergeRequestButtons = true;
- props.diffFile.editPath = '/';
+ props.diffFile.edit_path = '/';
vm = mountComponentWithStore(Component, { props, store });
expect(vm.$el.querySelector('.js-edit-blob')).toContainText('Edit');
@@ -386,8 +385,8 @@ describe('diff_file_header', () => {
it('should not show edit button when file is deleted', () => {
props.addMergeRequestButtons = true;
- props.diffFile.deletedFile = true;
- props.diffFile.editPath = '/';
+ props.diffFile.deleted_file = true;
+ props.diffFile.edit_path = '/';
vm = mountComponentWithStore(Component, { props, store });
expect(vm.$el.querySelector('.js-edit-blob')).toEqual(null);
@@ -397,7 +396,7 @@ describe('diff_file_header', () => {
describe('addMergeRequestButtons', () => {
beforeEach(() => {
props.addMergeRequestButtons = true;
- props.diffFile.editPath = '';
+ props.diffFile.edit_path = '';
});
describe('view on environment button', () => {
@@ -405,8 +404,8 @@ describe('diff_file_header', () => {
const title = 'url.title';
it('displays link to external url', () => {
- props.diffFile.externalUrl = url;
- props.diffFile.formattedExternalUrl = title;
+ props.diffFile.external_url = url;
+ props.diffFile.formatted_external_url = title;
vm = mountComponentWithStore(Component, { props, store });
@@ -415,8 +414,8 @@ describe('diff_file_header', () => {
});
it('hides link if no external url', () => {
- props.diffFile.externalUrl = '';
- props.diffFile.formattedExternalUrl = title;
+ props.diffFile.external_url = '';
+ props.diffFile.formattedExternal_url = title;
vm = mountComponentWithStore(Component, { props, store });
@@ -434,11 +433,11 @@ describe('diff_file_header', () => {
path: 'lib/base.js',
name: 'base.js',
mode: '100644',
- readableText: true,
+ readable_text: true,
icon: 'file-text-o',
};
propsCopy.addMergeRequestButtons = true;
- propsCopy.diffFile.deletedFile = true;
+ propsCopy.diffFile.deleted_file = true;
vm = mountComponentWithStore(Component, {
props: propsCopy,
@@ -459,11 +458,11 @@ describe('diff_file_header', () => {
path: 'lib/base.js',
name: 'base.js',
mode: '100644',
- readableText: true,
+ readable_text: true,
icon: 'file-text-o',
};
propsCopy.addMergeRequestButtons = true;
- propsCopy.diffFile.deletedFile = true;
+ propsCopy.diffFile.deleted_file = true;
const discussionGetter = () => [diffDiscussionMock];
const notesModuleMock = notesModule();
diff --git a/spec/javascripts/diffs/components/diff_file_spec.js b/spec/javascripts/diffs/components/diff_file_spec.js
index 882ad3685a2..51bb4807960 100644
--- a/spec/javascripts/diffs/components/diff_file_spec.js
+++ b/spec/javascripts/diffs/components/diff_file_spec.js
@@ -17,14 +17,14 @@ describe('DiffFile', () => {
describe('template', () => {
it('should render component with file header, file content components', () => {
const el = vm.$el;
- const { fileHash, filePath } = vm.file;
+ const { file_hash, file_path } = vm.file;
- expect(el.id).toEqual(fileHash);
+ expect(el.id).toEqual(file_hash);
expect(el.classList.contains('diff-file')).toEqual(true);
expect(el.querySelectorAll('.diff-content.hidden').length).toEqual(0);
expect(el.querySelector('.js-file-title')).toBeDefined();
- expect(el.querySelector('.file-title-name').innerText.indexOf(filePath)).toBeGreaterThan(-1);
+ expect(el.querySelector('.file-title-name').innerText.indexOf(file_path)).toBeGreaterThan(-1);
expect(el.querySelector('.js-syntax-highlight')).toBeDefined();
expect(vm.file.renderIt).toEqual(false);
@@ -52,7 +52,7 @@ describe('DiffFile', () => {
it('should have collapsed text and link', done => {
vm.file.renderIt = true;
vm.file.collapsed = false;
- vm.file.highlightedDiffLines = null;
+ vm.file.highlighted_diff_lines = null;
vm.$nextTick(() => {
expect(vm.$el.innerText).toContain('This diff is collapsed');
@@ -90,8 +90,8 @@ describe('DiffFile', () => {
describe('too large diff', () => {
it('should have too large warning and blob link', done => {
const BLOB_LINK = '/file/view/path';
- vm.file.tooLarge = true;
- vm.file.viewPath = BLOB_LINK;
+ vm.file.too_large = true;
+ vm.file.view_path = BLOB_LINK;
vm.$nextTick(() => {
expect(vm.$el.innerText).toContain(
@@ -107,4 +107,26 @@ describe('DiffFile', () => {
});
});
});
+
+ describe('watch collapsed', () => {
+ it('calls handleLoadCollapsedDiff if collapsed changed & file has no lines', done => {
+ spyOn(vm, 'handleLoadCollapsedDiff');
+
+ vm.file.highlighted_diff_lines = undefined;
+ vm.file.parallel_diff_lines = [];
+ vm.file.collapsed = true;
+
+ vm.$nextTick()
+ .then(() => {
+ vm.file.collapsed = false;
+
+ return vm.$nextTick();
+ })
+ .then(() => {
+ expect(vm.handleLoadCollapsedDiff).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
});
diff --git a/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js b/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js
index 6972e0ee913..038db8eaa7c 100644
--- a/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js
+++ b/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js
@@ -11,16 +11,16 @@ describe('DiffLineGutterContent', () => {
const cmp = Vue.extend(DiffLineGutterContent);
const props = Object.assign({}, options);
props.line = {
- lineCode: 'LC_42',
+ line_code: 'LC_42',
type: 'new',
- oldLine: null,
- newLine: 1,
- discussions: [],
+ old_line: null,
+ new_line: 1,
+ discussions: [{ ...discussionsMockData }],
text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
- richText: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
- metaData: null,
+ rich_text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ meta_data: null,
};
- props.fileHash = getDiffFileMock().fileHash;
+ props.fileHash = getDiffFileMock().file_hash;
props.contextLinesPath = '/context/lines/path';
return createComponentWithStore(cmp, store, props).$mount();
@@ -37,7 +37,7 @@ describe('DiffLineGutterContent', () => {
it('should return # if there is no lineCode', () => {
const component = createComponent();
- component.line.lineCode = '';
+ component.line.line_code = '';
expect(component.lineHref).toEqual('#');
});
@@ -46,6 +46,7 @@ describe('DiffLineGutterContent', () => {
describe('discussions, hasDiscussions, shouldShowAvatarsOnGutter', () => {
it('should return empty array when there is no discussion', () => {
const component = createComponent();
+ component.line.discussions = [];
expect(component.hasDiscussions).toEqual(false);
expect(component.shouldShowAvatarsOnGutter).toEqual(false);
@@ -54,8 +55,8 @@ describe('DiffLineGutterContent', () => {
it('should return discussions for the given lineCode', () => {
const cmp = Vue.extend(DiffLineGutterContent);
const props = {
- line: getDiffFileMock().highlightedDiffLines[1],
- fileHash: getDiffFileMock().fileHash,
+ line: getDiffFileMock().highlighted_diff_lines[1],
+ fileHash: getDiffFileMock().file_hash,
showCommentButton: true,
contextLinesPath: '/context/lines/path',
};
@@ -104,10 +105,10 @@ describe('DiffLineGutterContent', () => {
it('should render user avatars', () => {
const component = createComponent({
showCommentButton: true,
- lineCode: getDiffFileMock().highlightedDiffLines[1].lineCode,
+ lineCode: getDiffFileMock().highlighted_diff_lines[1].line_code,
});
- expect(component.$el.querySelector('.diff-comment-avatar-holders')).toBeDefined();
+ expect(component.$el.querySelector('.diff-comment-avatar-holders')).not.toBe(null);
});
});
});
diff --git a/spec/javascripts/diffs/components/diff_line_note_form_spec.js b/spec/javascripts/diffs/components/diff_line_note_form_spec.js
index c39b54d9cc9..81b66cf7c9b 100644
--- a/spec/javascripts/diffs/components/diff_line_note_form_spec.js
+++ b/spec/javascripts/diffs/components/diff_line_note_form_spec.js
@@ -13,10 +13,10 @@ describe('DiffLineNoteForm', () => {
beforeEach(() => {
diffFile = getDiffFileMock();
- diffLines = diffFile.highlightedDiffLines;
+ diffLines = diffFile.highlighted_diff_lines;
component = createComponentWithStore(Vue.extend(DiffLineNoteForm), store, {
- diffFileHash: diffFile.fileHash,
+ diffFileHash: diffFile.file_hash,
diffLines,
line: diffLines[0],
noteTargetLine: diffLines[0],
@@ -61,7 +61,7 @@ describe('DiffLineNoteForm', () => {
expect(window.confirm).not.toHaveBeenCalled();
component.$nextTick(() => {
expect(component.cancelCommentForm).toHaveBeenCalledWith({
- lineCode: diffLines[0].lineCode,
+ lineCode: diffLines[0].line_code,
});
expect(component.resetAutoSave).toHaveBeenCalled();
diff --git a/spec/javascripts/diffs/components/image_diff_overlay_spec.js b/spec/javascripts/diffs/components/image_diff_overlay_spec.js
new file mode 100644
index 00000000000..d76ab745fe1
--- /dev/null
+++ b/spec/javascripts/diffs/components/image_diff_overlay_spec.js
@@ -0,0 +1,146 @@
+import Vue from 'vue';
+import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
+import { createStore } from '~/mr_notes/stores';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { imageDiffDiscussions } from '../mock_data/diff_discussions';
+
+describe('Diffs image diff overlay component', () => {
+ const dimensions = {
+ width: 100,
+ height: 200,
+ };
+ let Component;
+ let vm;
+
+ function createComponent(props = {}, extendStore = () => {}) {
+ const store = createStore();
+
+ extendStore(store);
+
+ vm = createComponentWithStore(Component, store, {
+ discussions: [...imageDiffDiscussions],
+ fileHash: 'ABC',
+ ...props,
+ });
+ }
+
+ beforeAll(() => {
+ Component = Vue.extend(ImageDiffOverlay);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders comment badges', () => {
+ createComponent();
+ spyOn(vm, 'getImageDimensions').and.returnValue(dimensions);
+ vm.$mount();
+
+ expect(vm.$el.querySelectorAll('.js-image-badge').length).toBe(2);
+ });
+
+ it('renders index of discussion in badge', () => {
+ createComponent();
+ spyOn(vm, 'getImageDimensions').and.returnValue(dimensions);
+ vm.$mount();
+
+ expect(vm.$el.querySelectorAll('.js-image-badge')[0].textContent.trim()).toBe('1');
+ expect(vm.$el.querySelectorAll('.js-image-badge')[1].textContent.trim()).toBe('2');
+ });
+
+ it('renders icon when showCommentIcon is true', () => {
+ createComponent({ showCommentIcon: true });
+ spyOn(vm, 'getImageDimensions').and.returnValue(dimensions);
+ vm.$mount();
+
+ expect(vm.$el.querySelector('.js-image-badge svg')).not.toBe(null);
+ });
+
+ it('sets badge comment positions', () => {
+ createComponent();
+ spyOn(vm, 'getImageDimensions').and.returnValue(dimensions);
+ vm.$mount();
+
+ expect(vm.$el.querySelectorAll('.js-image-badge')[0].style.left).toBe('10px');
+ expect(vm.$el.querySelectorAll('.js-image-badge')[0].style.top).toBe('10px');
+
+ expect(vm.$el.querySelectorAll('.js-image-badge')[1].style.left).toBe('5px');
+ expect(vm.$el.querySelectorAll('.js-image-badge')[1].style.top).toBe('5px');
+ });
+
+ it('renders single badge for discussion object', () => {
+ createComponent({
+ discussions: {
+ ...imageDiffDiscussions[0],
+ },
+ });
+ spyOn(vm, 'getImageDimensions').and.returnValue(dimensions);
+ vm.$mount();
+
+ expect(vm.$el.querySelectorAll('.js-image-badge').length).toBe(1);
+ });
+
+ it('dispatches openDiffFileCommentForm when clicking overlay', () => {
+ createComponent({ canComment: true });
+ spyOn(vm, 'getImageDimensions').and.returnValue(dimensions);
+ vm.$mount();
+
+ spyOn(vm.$store, 'dispatch').and.stub();
+
+ vm.$el.querySelector('.js-add-image-diff-note-button').click();
+
+ expect(vm.$store.dispatch).toHaveBeenCalledWith('diffs/openDiffFileCommentForm', {
+ fileHash: 'ABC',
+ x: 0,
+ y: 0,
+ width: 100,
+ height: 200,
+ });
+ });
+
+ describe('toggle discussion', () => {
+ it('disables buttons when shouldToggleDiscussion is false', () => {
+ createComponent({ shouldToggleDiscussion: false });
+ spyOn(vm, 'getImageDimensions').and.returnValue(dimensions);
+ vm.$mount();
+
+ expect(vm.$el.querySelector('.js-image-badge').hasAttribute('disabled')).toBe(true);
+ });
+
+ it('dispatches toggleDiscussion when clicking image badge', () => {
+ createComponent();
+ spyOn(vm, 'getImageDimensions').and.returnValue(dimensions);
+ vm.$mount();
+
+ spyOn(vm.$store, 'dispatch').and.stub();
+
+ vm.$el.querySelector('.js-image-badge').click();
+
+ expect(vm.$store.dispatch).toHaveBeenCalledWith('toggleDiscussion', { discussionId: '1' });
+ });
+ });
+
+ describe('comment form', () => {
+ beforeEach(() => {
+ createComponent({}, store => {
+ store.state.diffs.commentForms.push({
+ fileHash: 'ABC',
+ x: 20,
+ y: 10,
+ });
+ });
+ spyOn(vm, 'getImageDimensions').and.returnValue(dimensions);
+ vm.$mount();
+ });
+
+ it('renders comment form badge', () => {
+ expect(vm.$el.querySelector('.comment-indicator')).not.toBe(null);
+ });
+
+ it('sets comment form badge position', () => {
+ expect(vm.$el.querySelector('.comment-indicator').style.left).toBe('20px');
+ expect(vm.$el.querySelector('.comment-indicator').style.top).toBe('10px');
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/components/inline_diff_view_spec.js b/spec/javascripts/diffs/components/inline_diff_view_spec.js
index 705558e860b..2316ee29106 100644
--- a/spec/javascripts/diffs/components/inline_diff_view_spec.js
+++ b/spec/javascripts/diffs/components/inline_diff_view_spec.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import '~/behaviors/markdown/render_gfm';
import InlineDiffView from '~/diffs/components/inline_diff_view.vue';
import store from '~/mr_notes/stores';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
@@ -10,14 +11,16 @@ describe('InlineDiffView', () => {
const getDiffFileMock = () => Object.assign({}, diffFileMockData);
const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)];
- beforeEach(() => {
+ beforeEach(done => {
const diffFile = getDiffFileMock();
store.dispatch('diffs/setInlineDiffViewType');
component = createComponentWithStore(Vue.extend(InlineDiffView), store, {
diffFile,
- diffLines: diffFile.highlightedDiffLines,
+ diffLines: diffFile.highlighted_diff_lines,
}).$mount();
+
+ Vue.nextTick(done);
});
describe('template', () => {
@@ -32,7 +35,7 @@ describe('InlineDiffView', () => {
it('should render discussions', done => {
const el = component.$el;
- component.$store.dispatch('setInitialNotes', getDiscussionsMockData());
+ component.diffLines[1].discussions = getDiscussionsMockData();
Vue.nextTick(() => {
expect(el.querySelectorAll('.notes_holder').length).toEqual(1);
diff --git a/spec/javascripts/diffs/components/parallel_diff_view_spec.js b/spec/javascripts/diffs/components/parallel_diff_view_spec.js
index 091e01868d3..6f6b1c41915 100644
--- a/spec/javascripts/diffs/components/parallel_diff_view_spec.js
+++ b/spec/javascripts/diffs/components/parallel_diff_view_spec.js
@@ -14,7 +14,7 @@ describe('ParallelDiffView', () => {
component = createComponentWithStore(Vue.extend(ParallelDiffView), store, {
diffFile,
- diffLines: diffFile.parallelDiffLines,
+ diffLines: diffFile.parallel_diff_lines,
}).$mount();
});
diff --git a/spec/javascripts/diffs/components/tree_list_spec.js b/spec/javascripts/diffs/components/tree_list_spec.js
index fc94d0bab5b..a0b380adfd6 100644
--- a/spec/javascripts/diffs/components/tree_list_spec.js
+++ b/spec/javascripts/diffs/components/tree_list_spec.js
@@ -81,7 +81,7 @@ describe('Diffs tree list component', () => {
});
it('filters tree list to blobs matching search', done => {
- vm.search = 'index';
+ vm.search = 'app/index';
vm.$nextTick(() => {
expect(vm.$el.querySelectorAll('.file-row').length).toBe(1);
diff --git a/spec/javascripts/diffs/mock_data/diff_discussions.js b/spec/javascripts/diffs/mock_data/diff_discussions.js
index 0ad214ea4a4..5ffe5a366ba 100644
--- a/spec/javascripts/diffs/mock_data/diff_discussions.js
+++ b/spec/javascripts/diffs/mock_data/diff_discussions.js
@@ -492,3 +492,24 @@ export default {
image_diff_html:
'<div class="image js-replaced-image" data="">\n<div class="two-up view">\n<div class="wrap">\n<div class="frame deleted">\n<img alt="CHANGELOG" src="http://localhost:3000/gitlab-org/gitlab-test/raw/e63f41fe459e62e1228fcef60d7189127aeba95a/CHANGELOG" />\n</div>\n<p class="image-info hide">\n<span class="meta-filesize">22.3 KB</span>\n|\n<strong>W:</strong>\n<span class="meta-width"></span>\n|\n<strong>H:</strong>\n<span class="meta-height"></span>\n</p>\n</div>\n<div class="wrap">\n<div class="added frame js-image-frame" data-note-type="DiffNote" data-position="{&quot;base_sha&quot;:&quot;e63f41fe459e62e1228fcef60d7189127aeba95a&quot;,&quot;start_sha&quot;:&quot;d9eaefe5a676b820c57ff18cf5b68316025f7962&quot;,&quot;head_sha&quot;:&quot;c48ee0d1bf3b30453f5b32250ce03134beaa6d13&quot;,&quot;old_path&quot;:&quot;CHANGELOG&quot;,&quot;new_path&quot;:&quot;CHANGELOG&quot;,&quot;position_type&quot;:&quot;text&quot;,&quot;old_line&quot;:null,&quot;new_line&quot;:2}">\n<img alt="CHANGELOG" draggable="false" src="http://localhost:3000/gitlab-org/gitlab-test/raw/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG" />\n</div>\n\n<p class="image-info hide">\n<span class="meta-filesize">22.3 KB</span>\n|\n<strong>W:</strong>\n<span class="meta-width"></span>\n|\n<strong>H:</strong>\n<span class="meta-height"></span>\n</p>\n</div>\n</div>\n<div class="swipe view hide">\n<div class="swipe-frame">\n<div class="frame deleted">\n<img alt="CHANGELOG" src="http://localhost:3000/gitlab-org/gitlab-test/raw/e63f41fe459e62e1228fcef60d7189127aeba95a/CHANGELOG" />\n</div>\n<div class="swipe-wrap">\n<div class="added frame js-image-frame" data-note-type="DiffNote" data-position="{&quot;base_sha&quot;:&quot;e63f41fe459e62e1228fcef60d7189127aeba95a&quot;,&quot;start_sha&quot;:&quot;d9eaefe5a676b820c57ff18cf5b68316025f7962&quot;,&quot;head_sha&quot;:&quot;c48ee0d1bf3b30453f5b32250ce03134beaa6d13&quot;,&quot;old_path&quot;:&quot;CHANGELOG&quot;,&quot;new_path&quot;:&quot;CHANGELOG&quot;,&quot;position_type&quot;:&quot;text&quot;,&quot;old_line&quot;:null,&quot;new_line&quot;:2}">\n<img alt="CHANGELOG" draggable="false" src="http://localhost:3000/gitlab-org/gitlab-test/raw/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG" />\n</div>\n\n</div>\n<span class="swipe-bar">\n<span class="top-handle"></span>\n<span class="bottom-handle"></span>\n</span>\n</div>\n</div>\n<div class="onion-skin view hide">\n<div class="onion-skin-frame">\n<div class="frame deleted">\n<img alt="CHANGELOG" src="http://localhost:3000/gitlab-org/gitlab-test/raw/e63f41fe459e62e1228fcef60d7189127aeba95a/CHANGELOG" />\n</div>\n<div class="added frame js-image-frame" data-note-type="DiffNote" data-position="{&quot;base_sha&quot;:&quot;e63f41fe459e62e1228fcef60d7189127aeba95a&quot;,&quot;start_sha&quot;:&quot;d9eaefe5a676b820c57ff18cf5b68316025f7962&quot;,&quot;head_sha&quot;:&quot;c48ee0d1bf3b30453f5b32250ce03134beaa6d13&quot;,&quot;old_path&quot;:&quot;CHANGELOG&quot;,&quot;new_path&quot;:&quot;CHANGELOG&quot;,&quot;position_type&quot;:&quot;text&quot;,&quot;old_line&quot;:null,&quot;new_line&quot;:2}">\n<img alt="CHANGELOG" draggable="false" src="http://localhost:3000/gitlab-org/gitlab-test/raw/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG" />\n</div>\n\n<div class="controls">\n<div class="transparent"></div>\n<div class="drag-track">\n<div class="dragger" style="left: 0px;"></div>\n</div>\n<div class="opaque"></div>\n</div>\n</div>\n</div>\n</div>\n<div class="view-modes hide">\n<ul class="view-modes-menu">\n<li class="two-up" data-mode="two-up">2-up</li>\n<li class="swipe" data-mode="swipe">Swipe</li>\n<li class="onion-skin" data-mode="onion-skin">Onion skin</li>\n</ul>\n</div>\n',
};
+
+export const imageDiffDiscussions = [
+ {
+ id: '1',
+ position: {
+ x: 10,
+ y: 10,
+ width: 100,
+ height: 200,
+ },
+ },
+ {
+ id: '2',
+ position: {
+ x: 5,
+ y: 5,
+ width: 100,
+ height: 200,
+ },
+ },
+];
diff --git a/spec/javascripts/diffs/mock_data/diff_file.js b/spec/javascripts/diffs/mock_data/diff_file.js
index d7bc0dbe431..031c9842f2f 100644
--- a/spec/javascripts/diffs/mock_data/diff_file.js
+++ b/spec/javascripts/diffs/mock_data/diff_file.js
@@ -1,130 +1,130 @@
export default {
submodule: false,
- submoduleLink: null,
+ submodule_link: null,
blob: {
id: '9e10516ca50788acf18c518a231914a21e5f16f7',
path: 'CHANGELOG',
name: 'CHANGELOG',
mode: '100644',
- readableText: true,
+ readable_text: true,
icon: 'file-text-o',
},
- blobPath: 'CHANGELOG',
- blobName: 'CHANGELOG',
- blobIcon: '<i aria-hidden="true" data-hidden="true" class="fa fa-file-text-o fa-fw"></i>',
- fileHash: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a',
- filePath: 'CHANGELOG',
- newFile: false,
- deletedFile: false,
- renamedFile: false,
- oldPath: 'CHANGELOG',
- newPath: 'CHANGELOG',
- modeChanged: false,
- aMode: '100644',
- bMode: '100644',
+ blob_path: 'CHANGELOG',
+ blob_name: 'CHANGELOG',
+ blob_icon: '<i aria-hidden="true" data-hidden="true" class="fa fa-file-text-o fa-fw"></i>',
+ file_hash: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a',
+ file_path: 'CHANGELOG',
+ new_file: false,
+ deleted_file: false,
+ renamed_file: false,
+ old_path: 'CHANGELOG',
+ new_path: 'CHANGELOG',
+ mode_changed: false,
+ a_mode: '100644',
+ b_mode: '100644',
text: true,
viewer: {
name: 'text',
},
- addedLines: 2,
- removedLines: 0,
- diffRefs: {
- baseSha: 'e63f41fe459e62e1228fcef60d7189127aeba95a',
- startSha: 'd9eaefe5a676b820c57ff18cf5b68316025f7962',
- headSha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13',
+ added_lines: 2,
+ removed_lines: 0,
+ diff_refs: {
+ base_sha: 'e63f41fe459e62e1228fcef60d7189127aeba95a',
+ start_sha: 'd9eaefe5a676b820c57ff18cf5b68316025f7962',
+ head_sha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13',
},
- contentSha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13',
- storedExternally: null,
- externalStorage: null,
- oldPathHtml: 'CHANGELOG',
- newPathHtml: 'CHANGELOG',
- editPath: '/gitlab-org/gitlab-test/edit/spooky-stuff/CHANGELOG',
- viewPath: '/gitlab-org/gitlab-test/blob/spooky-stuff/CHANGELOG',
- replacedViewPath: null,
+ content_sha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13',
+ stored_externally: null,
+ external_storage: null,
+ old_path_html: 'CHANGELOG',
+ new_path_html: 'CHANGELOG',
+ edit_path: '/gitlab-org/gitlab-test/edit/spooky-stuff/CHANGELOG',
+ view_path: '/gitlab-org/gitlab-test/blob/spooky-stuff/CHANGELOG',
+ replaced_view_path: null,
collapsed: false,
renderIt: false,
- tooLarge: false,
- contextLinesPath:
+ too_large: false,
+ context_lines_path:
'/gitlab-org/gitlab-test/blob/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG/diff',
- highlightedDiffLines: [
+ highlighted_diff_lines: [
{
- lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1',
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1',
type: 'new',
- oldLine: null,
- newLine: 1,
+ old_line: null,
+ new_line: 1,
discussions: [],
text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
- richText: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
- metaData: null,
+ rich_text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ meta_data: null,
},
{
- lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2',
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2',
type: 'new',
- oldLine: null,
- newLine: 2,
+ old_line: null,
+ new_line: 2,
discussions: [],
text: '+<span id="LC2" class="line" lang="plaintext"></span>\n',
- richText: '+<span id="LC2" class="line" lang="plaintext"></span>\n',
- metaData: null,
+ rich_text: '+<span id="LC2" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
},
{
- lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
type: null,
- oldLine: 1,
- newLine: 3,
+ old_line: 1,
+ new_line: 3,
discussions: [],
text: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
- richText: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
- metaData: null,
+ rich_text: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ meta_data: null,
},
{
- lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4',
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4',
type: null,
- oldLine: 2,
- newLine: 4,
+ old_line: 2,
+ new_line: 4,
discussions: [],
text: ' <span id="LC4" class="line" lang="plaintext"></span>\n',
- richText: ' <span id="LC4" class="line" lang="plaintext"></span>\n',
- metaData: null,
+ rich_text: ' <span id="LC4" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
},
{
- lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5',
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5',
type: null,
- oldLine: 3,
- newLine: 5,
+ old_line: 3,
+ new_line: 5,
discussions: [],
text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
- richText: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
- metaData: null,
+ rich_text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ meta_data: null,
},
{
- lineCode: null,
+ line_code: null,
type: 'match',
- oldLine: null,
- newLine: null,
+ old_line: null,
+ new_line: null,
discussions: [],
text: '',
- richText: '',
- metaData: {
- oldPos: 3,
- newPos: 5,
+ rich_text: '',
+ meta_data: {
+ old_pos: 3,
+ new_pos: 5,
},
},
],
- parallelDiffLines: [
+ parallel_diff_lines: [
{
left: {
type: 'empty-cell',
},
right: {
- lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1',
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1',
type: 'new',
- oldLine: null,
- newLine: 1,
+ old_line: null,
+ new_line: 1,
discussions: [],
text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
- richText: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
- metaData: null,
+ rich_text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ meta_data: null,
},
},
{
@@ -132,109 +132,110 @@ export default {
type: 'empty-cell',
},
right: {
- lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2',
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2',
type: 'new',
- oldLine: null,
- newLine: 2,
+ old_line: null,
+ new_line: 2,
discussions: [],
text: '+<span id="LC2" class="line" lang="plaintext"></span>\n',
- richText: '<span id="LC2" class="line" lang="plaintext"></span>\n',
- metaData: null,
+ rich_text: '<span id="LC2" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
},
},
{
left: {
- lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
+ line_Code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
type: null,
- oldLine: 1,
- newLine: 3,
+ old_line: 1,
+ new_line: 3,
discussions: [],
text: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
- richText: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
- metaData: null,
+ rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ meta_data: null,
},
right: {
- lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
type: null,
- oldLine: 1,
- newLine: 3,
+ old_line: 1,
+ new_line: 3,
discussions: [],
text: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
- richText: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
- metaData: null,
+ rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ meta_data: null,
},
},
{
left: {
- lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4',
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4',
type: null,
- oldLine: 2,
- newLine: 4,
+ old_line: 2,
+ new_line: 4,
discussions: [],
text: ' <span id="LC4" class="line" lang="plaintext"></span>\n',
- richText: '<span id="LC4" class="line" lang="plaintext"></span>\n',
- metaData: null,
+ rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
},
right: {
- lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4',
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4',
type: null,
- oldLine: 2,
- newLine: 4,
+ old_line: 2,
+ new_line: 4,
discussions: [],
text: ' <span id="LC4" class="line" lang="plaintext"></span>\n',
- richText: '<span id="LC4" class="line" lang="plaintext"></span>\n',
- metaData: null,
+ rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
},
},
{
left: {
- lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5',
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5',
type: null,
- oldLine: 3,
- newLine: 5,
+ old_line: 3,
+ new_line: 5,
discussions: [],
text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
- richText: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
- metaData: null,
+ rich_text: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ meta_data: null,
},
right: {
- lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5',
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5',
type: null,
- oldLine: 3,
- newLine: 5,
+ old_line: 3,
+ new_line: 5,
discussions: [],
text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
- richText: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
- metaData: null,
+ rich_text: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ meta_data: null,
},
},
{
left: {
- lineCode: null,
+ line_code: null,
type: 'match',
- oldLine: null,
- newLine: null,
+ old_line: null,
+ new_line: null,
discussions: [],
text: '',
- richText: '',
- metaData: {
- oldPos: 3,
- newPos: 5,
+ rich_text: '',
+ meta_data: {
+ old_pos: 3,
+ new_pos: 5,
},
},
right: {
- lineCode: null,
+ line_code: null,
type: 'match',
- oldLine: null,
- newLine: null,
+ old_line: null,
+ new_line: null,
discussions: [],
text: '',
- richText: '',
- metaData: {
- oldPos: 3,
- newPos: 5,
+ rich_text: '',
+ meta_data: {
+ old_pos: 3,
+ new_pos: 5,
},
},
},
],
+ discussions: [],
};
diff --git a/spec/javascripts/diffs/mock_data/diff_with_commit.js b/spec/javascripts/diffs/mock_data/diff_with_commit.js
index bee04fa4932..d646294ee84 100644
--- a/spec/javascripts/diffs/mock_data/diff_with_commit.js
+++ b/spec/javascripts/diffs/mock_data/diff_with_commit.js
@@ -1,9 +1,7 @@
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-
const FIXTURE = 'merge_request_diffs/with_commit.json';
preloadFixtures(FIXTURE);
export default function getDiffWithCommit() {
- return convertObjectPropsToCamelCase(getJSONFixture(FIXTURE), { deep: true });
+ return getJSONFixture(FIXTURE);
}
diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js
index bb623953710..d94a9cd1710 100644
--- a/spec/javascripts/diffs/store/actions_spec.js
+++ b/spec/javascripts/diffs/store/actions_spec.js
@@ -97,46 +97,46 @@ describe('DiffsStoreActions', () => {
const state = {
diffFiles: [
{
- fileHash: 'ABC',
- parallelDiffLines: [
+ file_hash: 'ABC',
+ parallel_diff_lines: [
{
left: {
- lineCode: 'ABC_1_1',
+ line_code: 'ABC_1_1',
discussions: [],
},
right: {
- lineCode: 'ABC_1_1',
+ line_code: 'ABC_1_1',
discussions: [],
},
},
],
- highlightedDiffLines: [
+ highlighted_diff_lines: [
{
- lineCode: 'ABC_1_1',
+ line_code: 'ABC_1_1',
discussions: [],
- oldLine: 5,
- newLine: null,
+ old_line: 5,
+ new_line: null,
},
],
- diffRefs: {
- baseSha: 'abc',
- headSha: 'def',
- startSha: 'ghi',
+ diff_refs: {
+ base_sha: 'abc',
+ head_sha: 'def',
+ start_sha: 'ghi',
},
- newPath: 'file1',
- oldPath: 'file2',
+ new_path: 'file1',
+ old_path: 'file2',
},
],
};
const diffPosition = {
- baseSha: 'abc',
- headSha: 'def',
- startSha: 'ghi',
- newLine: null,
- newPath: 'file1',
- oldLine: 5,
- oldPath: 'file2',
+ base_sha: 'abc',
+ head_sha: 'def',
+ start_sha: 'ghi',
+ new_line: null,
+ new_path: 'file1',
+ old_line: 5,
+ old_path: 'file2',
};
const singleDiscussion = {
@@ -145,7 +145,7 @@ describe('DiffsStoreActions', () => {
diff_file: {
file_hash: 'ABC',
},
- fileHash: 'ABC',
+ file_hash: 'ABC',
resolvable: true,
position: diffPosition,
original_position: diffPosition,
@@ -164,24 +164,22 @@ describe('DiffsStoreActions', () => {
discussion: singleDiscussion,
diffPositionByLineCode: {
ABC_1_1: {
- baseSha: 'abc',
- headSha: 'def',
- startSha: 'ghi',
- newLine: null,
- newPath: 'file1',
- oldLine: 5,
- oldPath: 'file2',
- lineCode: 'ABC_1_1',
- positionType: 'text',
+ base_sha: 'abc',
+ head_sha: 'def',
+ start_sha: 'ghi',
+ new_line: null,
+ new_path: 'file1',
+ old_line: 5,
+ old_path: 'file2',
+ line_code: 'ABC_1_1',
+ position_type: 'text',
},
},
},
},
],
[],
- () => {
- done();
- },
+ done,
);
});
});
@@ -191,11 +189,11 @@ describe('DiffsStoreActions', () => {
const state = {
diffFiles: [
{
- fileHash: 'ABC',
- parallelDiffLines: [
+ file_hash: 'ABC',
+ parallel_diff_lines: [
{
left: {
- lineCode: 'ABC_1_1',
+ line_code: 'ABC_1_1',
discussions: [
{
id: 1,
@@ -203,14 +201,14 @@ describe('DiffsStoreActions', () => {
],
},
right: {
- lineCode: 'ABC_1_1',
+ line_code: 'ABC_1_1',
discussions: [],
},
},
],
- highlightedDiffLines: [
+ highlighted_diff_lines: [
{
- lineCode: 'ABC_1_1',
+ line_code: 'ABC_1_1',
discussions: [],
},
],
@@ -218,7 +216,8 @@ describe('DiffsStoreActions', () => {
],
};
const singleDiscussion = {
- fileHash: 'ABC',
+ id: '1',
+ file_hash: 'ABC',
line_code: 'ABC_1_1',
};
@@ -230,15 +229,14 @@ describe('DiffsStoreActions', () => {
{
type: types.REMOVE_LINE_DISCUSSIONS_FOR_FILE,
payload: {
+ id: '1',
fileHash: 'ABC',
lineCode: 'ABC_1_1',
},
},
],
[],
- () => {
- done();
- },
+ done,
);
});
});
diff --git a/spec/javascripts/diffs/store/getters_spec.js b/spec/javascripts/diffs/store/getters_spec.js
index 807a9e3baf0..2449bb65d07 100644
--- a/spec/javascripts/diffs/store/getters_spec.js
+++ b/spec/javascripts/diffs/store/getters_spec.js
@@ -49,17 +49,17 @@ describe('Diffs Module Getters', () => {
});
});
- describe('areAllFilesCollapsed', () => {
+ describe('hasCollapsedFile', () => {
it('returns true when all files are collapsed', () => {
localState.diffFiles = [{ collapsed: true }, { collapsed: true }];
- expect(getters.areAllFilesCollapsed(localState)).toEqual(true);
+ expect(getters.hasCollapsedFile(localState)).toEqual(true);
});
- it('returns false when at least one file is not collapsed', () => {
+ it('returns true when at least one file is collapsed', () => {
localState.diffFiles = [{ collapsed: false }, { collapsed: true }];
- expect(getters.areAllFilesCollapsed(localState)).toEqual(false);
+ expect(getters.hasCollapsedFile(localState)).toEqual(true);
});
});
@@ -195,12 +195,12 @@ describe('Diffs Module Getters', () => {
discussionMock.expanded = true;
line.left = {
- lineCode: 'ABC',
+ line_code: 'ABC',
discussions: [discussionMock],
};
line.right = {
- lineCode: 'DEF',
+ line_code: 'DEF',
discussions: [discussionMock1],
};
});
@@ -259,7 +259,7 @@ describe('Diffs Module Getters', () => {
describe('getDiffFileDiscussions', () => {
it('returns an array with discussions when fileHash matches and the discussion belongs to a diff', () => {
- discussionMock.diff_file.file_hash = diffFileMock.fileHash;
+ discussionMock.diff_file.file_hash = diffFileMock.file_hash;
expect(
getters.getDiffFileDiscussions(localState, {}, {}, { discussions: [discussionMock] })(
@@ -279,10 +279,10 @@ describe('Diffs Module Getters', () => {
describe('getDiffFileByHash', () => {
it('returns file by hash', () => {
const fileA = {
- fileHash: '123',
+ file_hash: '123',
};
const fileB = {
- fileHash: '456',
+ file_hash: '456',
};
localState.diffFiles = [fileA, fileB];
diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js
index fed04cbaed8..598d723c940 100644
--- a/spec/javascripts/diffs/store/mutations_spec.js
+++ b/spec/javascripts/diffs/store/mutations_spec.js
@@ -37,7 +37,7 @@ describe('DiffsStoreMutations', () => {
mutations[types.SET_DIFF_DATA](state, diffMock);
- const firstLine = state.diffFiles[0].parallelDiffLines[0];
+ const firstLine = state.diffFiles[0].parallel_diff_lines[0];
expect(firstLine.right.text).toBeUndefined();
expect(state.diffFiles[0].renderIt).toEqual(true);
@@ -98,19 +98,19 @@ describe('DiffsStoreMutations', () => {
it('should call utils.addContextLines with proper params', () => {
const options = {
lineNumbers: { oldLineNumber: 1, newLineNumber: 2 },
- contextLines: [{ oldLine: 1 }],
+ contextLines: [{ old_line: 1, new_line: 1, line_code: 'ff9200_1_1', discussions: [] }],
fileHash: 'ff9200',
params: {
bottom: true,
},
};
const diffFile = {
- fileHash: options.fileHash,
- highlightedDiffLines: [],
- parallelDiffLines: [],
+ file_hash: options.fileHash,
+ highlighted_diff_lines: [],
+ parallel_diff_lines: [],
};
const state = { diffFiles: [diffFile] };
- const lines = [{ oldLine: 1 }];
+ const lines = [{ old_line: 1, new_line: 1 }];
const findDiffFileSpy = spyOnDependency(mutations, 'findDiffFile').and.returnValue(diffFile);
const removeMatchLineSpy = spyOnDependency(mutations, 'removeMatchLine');
@@ -133,8 +133,8 @@ describe('DiffsStoreMutations', () => {
);
expect(addContextLinesSpy).toHaveBeenCalledWith({
- inlineLines: diffFile.highlightedDiffLines,
- parallelLines: diffFile.parallelDiffLines,
+ inlineLines: diffFile.highlighted_diff_lines,
+ parallelLines: diffFile.parallel_diff_lines,
contextLines: options.contextLines,
bottom: options.params.bottom,
lineNumbers: options.lineNumbers,
@@ -144,54 +144,50 @@ describe('DiffsStoreMutations', () => {
describe('ADD_COLLAPSED_DIFFS', () => {
it('should update the state with the given data for the given file hash', () => {
- const spy = spyOnDependency(mutations, 'convertObjectPropsToCamelCase').and.callThrough();
-
const fileHash = 123;
- const state = { diffFiles: [{}, { fileHash, existingField: 0 }] };
- const data = { diff_files: [{ file_hash: fileHash, extra_field: 1, existingField: 1 }] };
+ const state = { diffFiles: [{}, { file_hash: fileHash, existing_field: 0 }] };
+ const data = { diff_files: [{ file_hash: fileHash, extra_field: 1, existing_field: 1 }] };
mutations[types.ADD_COLLAPSED_DIFFS](state, { file: state.diffFiles[1], data });
- expect(spy).toHaveBeenCalledWith(data, { deep: true });
-
- expect(state.diffFiles[1].fileHash).toEqual(fileHash);
- expect(state.diffFiles[1].existingField).toEqual(1);
- expect(state.diffFiles[1].extraField).toEqual(1);
+ expect(state.diffFiles[1].file_hash).toEqual(fileHash);
+ expect(state.diffFiles[1].existing_field).toEqual(1);
+ expect(state.diffFiles[1].extra_field).toEqual(1);
});
});
describe('SET_LINE_DISCUSSIONS_FOR_FILE', () => {
it('should add discussions to the given line', () => {
const diffPosition = {
- baseSha: 'ed13df29948c41ba367caa757ab3ec4892509910',
- headSha: 'b921914f9a834ac47e6fd9420f78db0f83559130',
- newLine: null,
- newPath: '500-lines-4.txt',
- oldLine: 5,
- oldPath: '500-lines-4.txt',
- startSha: 'ed13df29948c41ba367caa757ab3ec4892509910',
+ base_sha: 'ed13df29948c41ba367caa757ab3ec4892509910',
+ head_sha: 'b921914f9a834ac47e6fd9420f78db0f83559130',
+ new_line: null,
+ new_path: '500-lines-4.txt',
+ old_line: 5,
+ old_path: '500-lines-4.txt',
+ start_sha: 'ed13df29948c41ba367caa757ab3ec4892509910',
};
const state = {
latestDiff: true,
diffFiles: [
{
- fileHash: 'ABC',
- parallelDiffLines: [
+ file_hash: 'ABC',
+ parallel_diff_lines: [
{
left: {
- lineCode: 'ABC_1',
+ line_code: 'ABC_1',
discussions: [],
},
right: {
- lineCode: 'ABC_1',
+ line_code: 'ABC_1',
discussions: [],
},
},
],
- highlightedDiffLines: [
+ highlighted_diff_lines: [
{
- lineCode: 'ABC_1',
+ line_code: 'ABC_1',
discussions: [],
},
],
@@ -206,7 +202,7 @@ describe('DiffsStoreMutations', () => {
original_position: diffPosition,
position: diffPosition,
diff_file: {
- file_hash: state.diffFiles[0].fileHash,
+ file_hash: state.diffFiles[0].file_hash,
},
};
@@ -219,46 +215,46 @@ describe('DiffsStoreMutations', () => {
diffPositionByLineCode,
});
- expect(state.diffFiles[0].parallelDiffLines[0].left.discussions.length).toEqual(1);
- expect(state.diffFiles[0].parallelDiffLines[0].left.discussions[0].id).toEqual(1);
- expect(state.diffFiles[0].parallelDiffLines[0].right.discussions).toEqual([]);
+ expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions.length).toEqual(1);
+ expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions[0].id).toEqual(1);
+ expect(state.diffFiles[0].parallel_diff_lines[0].right.discussions).toEqual([]);
- expect(state.diffFiles[0].highlightedDiffLines[0].discussions.length).toEqual(1);
- expect(state.diffFiles[0].highlightedDiffLines[0].discussions[0].id).toEqual(1);
+ expect(state.diffFiles[0].highlighted_diff_lines[0].discussions.length).toEqual(1);
+ expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].id).toEqual(1);
});
it('should add legacy discussions to the given line', () => {
const diffPosition = {
- baseSha: 'ed13df29948c41ba367caa757ab3ec4892509910',
- headSha: 'b921914f9a834ac47e6fd9420f78db0f83559130',
- newLine: null,
- newPath: '500-lines-4.txt',
- oldLine: 5,
- oldPath: '500-lines-4.txt',
- startSha: 'ed13df29948c41ba367caa757ab3ec4892509910',
- lineCode: 'ABC_1',
+ base_sha: 'ed13df29948c41ba367caa757ab3ec4892509910',
+ head_sha: 'b921914f9a834ac47e6fd9420f78db0f83559130',
+ new_line: null,
+ new_path: '500-lines-4.txt',
+ old_line: 5,
+ old_path: '500-lines-4.txt',
+ start_sha: 'ed13df29948c41ba367caa757ab3ec4892509910',
+ line_code: 'ABC_1',
};
const state = {
latestDiff: true,
diffFiles: [
{
- fileHash: 'ABC',
- parallelDiffLines: [
+ file_hash: 'ABC',
+ parallel_diff_lines: [
{
left: {
- lineCode: 'ABC_1',
+ line_code: 'ABC_1',
discussions: [],
},
right: {
- lineCode: 'ABC_1',
+ line_code: 'ABC_1',
discussions: [],
},
},
],
- highlightedDiffLines: [
+ highlighted_diff_lines: [
{
- lineCode: 'ABC_1',
+ line_code: 'ABC_1',
discussions: [],
},
],
@@ -271,7 +267,7 @@ describe('DiffsStoreMutations', () => {
diff_discussion: true,
active: true,
diff_file: {
- file_hash: state.diffFiles[0].fileHash,
+ file_hash: state.diffFiles[0].file_hash,
},
};
@@ -284,11 +280,11 @@ describe('DiffsStoreMutations', () => {
diffPositionByLineCode,
});
- expect(state.diffFiles[0].parallelDiffLines[0].left.discussions.length).toEqual(1);
- expect(state.diffFiles[0].parallelDiffLines[0].left.discussions[0].id).toEqual(1);
+ expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions.length).toEqual(1);
+ expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions[0].id).toEqual(1);
- expect(state.diffFiles[0].highlightedDiffLines[0].discussions.length).toEqual(1);
- expect(state.diffFiles[0].highlightedDiffLines[0].discussions[0].id).toEqual(1);
+ expect(state.diffFiles[0].highlighted_diff_lines[0].discussions.length).toEqual(1);
+ expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].id).toEqual(1);
});
});
@@ -297,11 +293,11 @@ describe('DiffsStoreMutations', () => {
const state = {
diffFiles: [
{
- fileHash: 'ABC',
- parallelDiffLines: [
+ file_hash: 'ABC',
+ parallel_diff_lines: [
{
left: {
- lineCode: 'ABC_1',
+ line_code: 'ABC_1',
discussions: [
{
id: 1,
@@ -314,14 +310,14 @@ describe('DiffsStoreMutations', () => {
],
},
right: {
- lineCode: 'ABC_1',
+ line_code: 'ABC_1',
discussions: [],
},
},
],
- highlightedDiffLines: [
+ highlighted_diff_lines: [
{
- lineCode: 'ABC_1',
+ line_code: 'ABC_1',
discussions: [
{
id: 1,
@@ -343,8 +339,8 @@ describe('DiffsStoreMutations', () => {
lineCode: 'ABC_1',
});
- expect(state.diffFiles[0].parallelDiffLines[0].left.discussions.length).toEqual(0);
- expect(state.diffFiles[0].highlightedDiffLines[0].discussions.length).toEqual(0);
+ expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions.length).toEqual(0);
+ expect(state.diffFiles[0].highlighted_diff_lines[0].discussions.length).toEqual(0);
});
});
diff --git a/spec/javascripts/diffs/store/utils_spec.js b/spec/javascripts/diffs/store/utils_spec.js
index f49dee3696d..d4ef17c5ef8 100644
--- a/spec/javascripts/diffs/store/utils_spec.js
+++ b/spec/javascripts/diffs/store/utils_spec.js
@@ -18,7 +18,7 @@ const getDiffFileMock = () => Object.assign({}, diffFileMockData);
describe('DiffsStoreUtils', () => {
describe('findDiffFile', () => {
- const files = [{ fileHash: 1, name: 'one' }];
+ const files = [{ file_hash: 1, name: 'one' }];
it('should return correct file', () => {
expect(utils.findDiffFile(files, 1).name).toEqual('one');
@@ -41,13 +41,13 @@ describe('DiffsStoreUtils', () => {
describe('findIndexInInlineLines', () => {
it('should return correct index for given line numbers', () => {
- expectSet(utils.findIndexInInlineLines, getDiffFileMock().highlightedDiffLines);
+ expectSet(utils.findIndexInInlineLines, getDiffFileMock().highlighted_diff_lines);
});
});
describe('findIndexInParallelLines', () => {
it('should return correct index for given line numbers', () => {
- expectSet(utils.findIndexInParallelLines, getDiffFileMock().parallelDiffLines, {});
+ expectSet(utils.findIndexInParallelLines, getDiffFileMock().parallel_diff_lines, {});
});
});
});
@@ -56,33 +56,39 @@ describe('DiffsStoreUtils', () => {
it('should remove match line properly by regarding the bottom parameter', () => {
const diffFile = getDiffFileMock();
const lineNumbers = { oldLineNumber: 3, newLineNumber: 5 };
- const inlineIndex = utils.findIndexInInlineLines(diffFile.highlightedDiffLines, lineNumbers);
- const parallelIndex = utils.findIndexInParallelLines(diffFile.parallelDiffLines, lineNumbers);
- const atInlineIndex = diffFile.highlightedDiffLines[inlineIndex];
- const atParallelIndex = diffFile.parallelDiffLines[parallelIndex];
+ const inlineIndex = utils.findIndexInInlineLines(
+ diffFile.highlighted_diff_lines,
+ lineNumbers,
+ );
+ const parallelIndex = utils.findIndexInParallelLines(
+ diffFile.parallel_diff_lines,
+ lineNumbers,
+ );
+ const atInlineIndex = diffFile.highlighted_diff_lines[inlineIndex];
+ const atParallelIndex = diffFile.parallel_diff_lines[parallelIndex];
utils.removeMatchLine(diffFile, lineNumbers, false);
- expect(diffFile.highlightedDiffLines[inlineIndex]).not.toEqual(atInlineIndex);
- expect(diffFile.parallelDiffLines[parallelIndex]).not.toEqual(atParallelIndex);
+ expect(diffFile.highlighted_diff_lines[inlineIndex]).not.toEqual(atInlineIndex);
+ expect(diffFile.parallel_diff_lines[parallelIndex]).not.toEqual(atParallelIndex);
utils.removeMatchLine(diffFile, lineNumbers, true);
- expect(diffFile.highlightedDiffLines[inlineIndex + 1]).not.toEqual(atInlineIndex);
- expect(diffFile.parallelDiffLines[parallelIndex + 1]).not.toEqual(atParallelIndex);
+ expect(diffFile.highlighted_diff_lines[inlineIndex + 1]).not.toEqual(atInlineIndex);
+ expect(diffFile.parallel_diff_lines[parallelIndex + 1]).not.toEqual(atParallelIndex);
});
});
describe('addContextLines', () => {
it('should add context lines properly with bottom parameter', () => {
const diffFile = getDiffFileMock();
- const inlineLines = diffFile.highlightedDiffLines;
- const parallelLines = diffFile.parallelDiffLines;
+ const inlineLines = diffFile.highlighted_diff_lines;
+ const parallelLines = diffFile.parallel_diff_lines;
const lineNumbers = { oldLineNumber: 3, newLineNumber: 5 };
const contextLines = [{ lineNumber: 42 }];
const options = { inlineLines, parallelLines, contextLines, lineNumbers, bottom: true };
- const inlineIndex = utils.findIndexInInlineLines(diffFile.highlightedDiffLines, lineNumbers);
- const parallelIndex = utils.findIndexInParallelLines(diffFile.parallelDiffLines, lineNumbers);
+ const inlineIndex = utils.findIndexInInlineLines(inlineLines, lineNumbers);
+ const parallelIndex = utils.findIndexInParallelLines(parallelLines, lineNumbers);
const normalizedParallelLine = {
left: options.contextLines[0],
right: options.contextLines[0],
@@ -112,30 +118,30 @@ describe('DiffsStoreUtils', () => {
noteableType: MERGE_REQUEST_NOTEABLE_TYPE,
diffFile,
noteTargetLine: {
- lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
- metaData: null,
- newLine: 3,
- oldLine: 1,
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
+ meta_data: null,
+ new_line: 3,
+ old_line: 1,
},
diffViewType: PARALLEL_DIFF_VIEW_TYPE,
linePosition: LINE_POSITION_LEFT,
};
const position = JSON.stringify({
- base_sha: diffFile.diffRefs.baseSha,
- start_sha: diffFile.diffRefs.startSha,
- head_sha: diffFile.diffRefs.headSha,
- old_path: diffFile.oldPath,
- new_path: diffFile.newPath,
+ base_sha: diffFile.diff_refs.base_sha,
+ start_sha: diffFile.diff_refs.start_sha,
+ head_sha: diffFile.diff_refs.head_sha,
+ old_path: diffFile.old_path,
+ new_path: diffFile.new_path,
position_type: TEXT_DIFF_POSITION_TYPE,
- old_line: options.noteTargetLine.oldLine,
- new_line: options.noteTargetLine.newLine,
+ old_line: options.noteTargetLine.old_line,
+ new_line: options.noteTargetLine.new_line,
});
const postData = {
view: options.diffViewType,
line_type: options.linePosition === LINE_POSITION_RIGHT ? NEW_LINE_TYPE : OLD_LINE_TYPE,
- merge_request_diff_head_sha: diffFile.diffRefs.headSha,
+ merge_request_diff_head_sha: diffFile.diff_refs.head_sha,
in_reply_to_discussion_id: '',
note_project_id: '',
target_type: options.noteableType,
@@ -146,7 +152,7 @@ describe('DiffsStoreUtils', () => {
noteable_id: options.noteableData.id,
commit_id: '',
type: DIFF_NOTE_TYPE,
- line_code: options.noteTargetLine.lineCode,
+ line_code: options.noteTargetLine.line_code,
note: options.note,
position,
},
@@ -160,8 +166,8 @@ describe('DiffsStoreUtils', () => {
it('should create legacy note form data', () => {
const diffFile = getDiffFileMock();
- delete diffFile.diffRefs.startSha;
- delete diffFile.diffRefs.headSha;
+ delete diffFile.diff_refs.start_sha;
+ delete diffFile.diff_refs.head_sha;
noteableDataMock.targetType = MERGE_REQUEST_NOTEABLE_TYPE;
@@ -171,24 +177,24 @@ describe('DiffsStoreUtils', () => {
noteableType: MERGE_REQUEST_NOTEABLE_TYPE,
diffFile,
noteTargetLine: {
- lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
- metaData: null,
- newLine: 3,
- oldLine: 1,
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
+ meta_data: null,
+ new_line: 3,
+ old_line: 1,
},
diffViewType: PARALLEL_DIFF_VIEW_TYPE,
linePosition: LINE_POSITION_LEFT,
};
const position = JSON.stringify({
- base_sha: diffFile.diffRefs.baseSha,
+ base_sha: diffFile.diff_refs.base_sha,
start_sha: undefined,
head_sha: undefined,
- old_path: diffFile.oldPath,
- new_path: diffFile.newPath,
+ old_path: diffFile.old_path,
+ new_path: diffFile.new_path,
position_type: TEXT_DIFF_POSITION_TYPE,
- old_line: options.noteTargetLine.oldLine,
- new_line: options.noteTargetLine.newLine,
+ old_line: options.noteTargetLine.old_line,
+ new_line: options.noteTargetLine.new_line,
});
const postData = {
@@ -205,7 +211,7 @@ describe('DiffsStoreUtils', () => {
noteable_id: options.noteableData.id,
commit_id: '',
type: LEGACY_DIFF_NOTE_TYPE,
- line_code: options.noteTargetLine.lineCode,
+ line_code: options.noteTargetLine.line_code,
note: options.note,
position,
},
@@ -225,61 +231,61 @@ describe('DiffsStoreUtils', () => {
const lines = [{ type: null }, { type: MATCH_LINE_TYPE }];
const linesWithReferences = utils.addLineReferences(lines, lineNumbers, true);
- expect(linesWithReferences[0].oldLine).toEqual(lineNumbers.oldLineNumber + 1);
- expect(linesWithReferences[0].newLine).toEqual(lineNumbers.newLineNumber + 1);
- expect(linesWithReferences[1].metaData.oldPos).toEqual(4);
- expect(linesWithReferences[1].metaData.newPos).toEqual(5);
+ expect(linesWithReferences[0].old_line).toEqual(lineNumbers.oldLineNumber + 1);
+ expect(linesWithReferences[0].new_line).toEqual(lineNumbers.newLineNumber + 1);
+ expect(linesWithReferences[1].meta_data.old_pos).toEqual(4);
+ expect(linesWithReferences[1].meta_data.new_pos).toEqual(5);
});
it('should add correct line references when bottom falsy', () => {
const lines = [{ type: null }, { type: MATCH_LINE_TYPE }, { type: null }];
const linesWithReferences = utils.addLineReferences(lines, lineNumbers);
- expect(linesWithReferences[0].oldLine).toEqual(0);
- expect(linesWithReferences[0].newLine).toEqual(1);
- expect(linesWithReferences[1].metaData.oldPos).toEqual(2);
- expect(linesWithReferences[1].metaData.newPos).toEqual(3);
+ expect(linesWithReferences[0].old_line).toEqual(0);
+ expect(linesWithReferences[0].new_line).toEqual(1);
+ expect(linesWithReferences[1].meta_data.old_pos).toEqual(2);
+ expect(linesWithReferences[1].meta_data.new_pos).toEqual(3);
});
});
describe('trimFirstCharOfLineContent', () => {
it('trims the line when it starts with a space', () => {
- expect(utils.trimFirstCharOfLineContent({ richText: ' diff' })).toEqual({
+ expect(utils.trimFirstCharOfLineContent({ rich_text: ' diff' })).toEqual({
discussions: [],
- richText: 'diff',
+ rich_text: 'diff',
});
});
it('trims the line when it starts with a +', () => {
- expect(utils.trimFirstCharOfLineContent({ richText: '+diff' })).toEqual({
+ expect(utils.trimFirstCharOfLineContent({ rich_text: '+diff' })).toEqual({
discussions: [],
- richText: 'diff',
+ rich_text: 'diff',
});
});
it('trims the line when it starts with a -', () => {
- expect(utils.trimFirstCharOfLineContent({ richText: '-diff' })).toEqual({
+ expect(utils.trimFirstCharOfLineContent({ rich_text: '-diff' })).toEqual({
discussions: [],
- richText: 'diff',
+ rich_text: 'diff',
});
});
it('does not trims the line when it starts with a letter', () => {
- expect(utils.trimFirstCharOfLineContent({ richText: 'diff' })).toEqual({
+ expect(utils.trimFirstCharOfLineContent({ rich_text: 'diff' })).toEqual({
discussions: [],
- richText: 'diff',
+ rich_text: 'diff',
});
});
it('does not modify the provided object', () => {
const lineObj = {
discussions: [],
- richText: ' diff',
+ rich_text: ' diff',
};
utils.trimFirstCharOfLineContent(lineObj);
- expect(lineObj).toEqual({ discussions: [], richText: ' diff' });
+ expect(lineObj).toEqual({ discussions: [], rich_text: ' diff' });
});
it('handles a undefined or null parameter', () => {
@@ -289,33 +295,33 @@ describe('DiffsStoreUtils', () => {
describe('prepareDiffData', () => {
it('sets the renderIt and collapsed attribute on files', () => {
- const preparedDiff = { diffFiles: [getDiffFileMock()] };
+ const preparedDiff = { diff_files: [getDiffFileMock()] };
utils.prepareDiffData(preparedDiff);
- const firstParallelDiffLine = preparedDiff.diffFiles[0].parallelDiffLines[2];
+ const firstParallelDiffLine = preparedDiff.diff_files[0].parallel_diff_lines[2];
expect(firstParallelDiffLine.left.discussions.length).toBe(0);
expect(firstParallelDiffLine.left).not.toHaveAttr('text');
expect(firstParallelDiffLine.right.discussions.length).toBe(0);
expect(firstParallelDiffLine.right).not.toHaveAttr('text');
- const firstParallelChar = firstParallelDiffLine.right.richText.charAt(0);
+ const firstParallelChar = firstParallelDiffLine.right.rich_text.charAt(0);
expect(firstParallelChar).not.toBe(' ');
expect(firstParallelChar).not.toBe('+');
expect(firstParallelChar).not.toBe('-');
- const checkLine = preparedDiff.diffFiles[0].highlightedDiffLines[0];
+ const checkLine = preparedDiff.diff_files[0].highlighted_diff_lines[0];
expect(checkLine.discussions.length).toBe(0);
expect(checkLine).not.toHaveAttr('text');
- const firstChar = checkLine.richText.charAt(0);
+ const firstChar = checkLine.rich_text.charAt(0);
expect(firstChar).not.toBe(' ');
expect(firstChar).not.toBe('+');
expect(firstChar).not.toBe('-');
- expect(preparedDiff.diffFiles[0].renderIt).toBeTruthy();
- expect(preparedDiff.diffFiles[0].collapsed).toBeFalsy();
+ expect(preparedDiff.diff_files[0].renderIt).toBeTruthy();
+ expect(preparedDiff.diff_files[0].collapsed).toBeFalsy();
});
});
@@ -398,7 +404,7 @@ describe('DiffsStoreUtils', () => {
discussion,
diffPosition: {
...diffPosition,
- lineCode: 'ABC_1',
+ line_code: 'ABC_1',
},
latestDiff: true,
}),
@@ -429,36 +435,36 @@ describe('DiffsStoreUtils', () => {
beforeAll(() => {
files = [
{
- newPath: 'app/index.js',
- deletedFile: false,
- newFile: false,
- removedLines: 10,
- addedLines: 0,
- fileHash: 'test',
+ new_path: 'app/index.js',
+ deleted_file: false,
+ new_file: false,
+ removed_lines: 10,
+ added_lines: 0,
+ file_hash: 'test',
},
{
- newPath: 'app/test/index.js',
- deletedFile: false,
- newFile: true,
- removedLines: 0,
- addedLines: 0,
- fileHash: 'test',
+ new_path: 'app/test/index.js',
+ deleted_file: false,
+ new_file: true,
+ removed_lines: 0,
+ added_lines: 0,
+ file_hash: 'test',
},
{
- newPath: 'app/test/filepathneedstruncating.js',
- deletedFile: false,
- newFile: true,
- removedLines: 0,
- addedLines: 0,
- fileHash: 'test',
+ new_path: 'app/test/filepathneedstruncating.js',
+ deleted_file: false,
+ new_file: true,
+ removed_lines: 0,
+ added_lines: 0,
+ file_hash: 'test',
},
{
- newPath: 'package.json',
- deletedFile: true,
- newFile: false,
- removedLines: 0,
- addedLines: 0,
- fileHash: 'test',
+ new_path: 'package.json',
+ deleted_file: true,
+ new_file: false,
+ removed_lines: 0,
+ added_lines: 0,
+ file_hash: 'test',
},
];
});
diff --git a/spec/javascripts/dirty_submit/dirty_submit_form_spec.js b/spec/javascripts/dirty_submit/dirty_submit_form_spec.js
index b7b29190c31..093fec97951 100644
--- a/spec/javascripts/dirty_submit/dirty_submit_form_spec.js
+++ b/spec/javascripts/dirty_submit/dirty_submit_form_spec.js
@@ -1,23 +1,35 @@
import DirtySubmitForm from '~/dirty_submit/dirty_submit_form';
import { setInput, createForm } from './helper';
+function expectToToggleDisableOnDirtyUpdate(submit, input) {
+ const originalValue = input.value;
+
+ expect(submit.disabled).toBe(true);
+
+ return setInput(input, `${originalValue} changes`)
+ .then(() => expect(submit.disabled).toBe(false))
+ .then(() => setInput(input, originalValue))
+ .then(() => expect(submit.disabled).toBe(true));
+}
+
describe('DirtySubmitForm', () => {
it('disables submit until there are changes', done => {
const { form, input, submit } = createForm();
- const originalValue = input.value;
new DirtySubmitForm(form); // eslint-disable-line no-new
- expect(submit.disabled).toBe(true);
+ return expectToToggleDisableOnDirtyUpdate(submit, input)
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('disables submit until there are changes when initializing with a falsy value', done => {
+ const { form, input, submit } = createForm();
+ input.value = '';
+
+ new DirtySubmitForm(form); // eslint-disable-line no-new
- return setInput(input, `${originalValue} changes`)
- .then(() => {
- expect(submit.disabled).toBe(false);
- })
- .then(() => setInput(input, originalValue))
- .then(() => {
- expect(submit.disabled).toBe(true);
- })
+ return expectToToggleDisableOnDirtyUpdate(submit, input)
.then(done)
.catch(done.fail);
});
diff --git a/spec/javascripts/environments/emtpy_state_spec.js b/spec/javascripts/environments/emtpy_state_spec.js
index d71dfe8197e..1f986d49bc7 100644
--- a/spec/javascripts/environments/emtpy_state_spec.js
+++ b/spec/javascripts/environments/emtpy_state_spec.js
@@ -1,4 +1,3 @@
-
import Vue from 'vue';
import emptyState from '~/environments/components/empty_state.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
@@ -25,13 +24,13 @@ describe('environments empty state', () => {
});
it('renders empty state and new environment button', () => {
- expect(
- vm.$el.querySelector('.js-blank-state-title').textContent.trim(),
- ).toEqual('You don\'t have any environments right now');
+ expect(vm.$el.querySelector('.js-blank-state-title').textContent.trim()).toEqual(
+ "You don't have any environments right now",
+ );
- expect(
- vm.$el.querySelector('.js-new-environment-button').getAttribute('href'),
- ).toEqual('foo');
+ expect(vm.$el.querySelector('.js-new-environment-button').getAttribute('href')).toEqual(
+ 'foo',
+ );
});
});
@@ -45,13 +44,11 @@ describe('environments empty state', () => {
});
it('renders empty state without new button', () => {
- expect(
- vm.$el.querySelector('.js-blank-state-title').textContent.trim(),
- ).toEqual('You don\'t have any environments right now');
+ expect(vm.$el.querySelector('.js-blank-state-title').textContent.trim()).toEqual(
+ "You don't have any environments right now",
+ );
- expect(
- vm.$el.querySelector('.js-new-environment-button'),
- ).toBeNull();
+ expect(vm.$el.querySelector('.js-new-environment-button')).toBeNull();
});
});
});
diff --git a/spec/javascripts/environments/environment_actions_spec.js b/spec/javascripts/environments/environment_actions_spec.js
index 223153d4e31..787df757d32 100644
--- a/spec/javascripts/environments/environment_actions_spec.js
+++ b/spec/javascripts/environments/environment_actions_spec.js
@@ -1,15 +1,19 @@
import Vue from 'vue';
-import actionsComp from '~/environments/components/environment_actions.vue';
+import eventHub from '~/environments/event_hub';
+import EnvironmentActions from '~/environments/components/environment_actions.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { TEST_HOST } from 'spec/test_constants';
-describe('Actions Component', () => {
- let ActionsComponent;
- let actionsMock;
- let component;
+describe('EnvironmentActions Component', () => {
+ const Component = Vue.extend(EnvironmentActions);
+ let vm;
- beforeEach(() => {
- ActionsComponent = Vue.extend(actionsComp);
+ afterEach(() => {
+ vm.$destroy();
+ });
- actionsMock = [
+ describe('manual actions', () => {
+ const actions = [
{
name: 'bar',
play_path: 'https://gitlab.com/play',
@@ -25,43 +29,89 @@ describe('Actions Component', () => {
},
];
- component = new ActionsComponent({
- propsData: {
- actions: actionsMock,
- },
- }).$mount();
- });
+ beforeEach(() => {
+ vm = mountComponent(Component, { actions });
+ });
+
+ it('should render a dropdown button with icon and title attribute', () => {
+ expect(vm.$el.querySelector('.fa-caret-down')).toBeDefined();
+ expect(vm.$el.querySelector('.dropdown-new').getAttribute('data-original-title')).toEqual(
+ 'Deploy to...',
+ );
- describe('computed', () => {
- it('title', () => {
- expect(component.title).toEqual('Deploy to...');
+ expect(vm.$el.querySelector('.dropdown-new').getAttribute('aria-label')).toEqual(
+ 'Deploy to...',
+ );
});
- });
- it('should render a dropdown button with icon and title attribute', () => {
- expect(component.$el.querySelector('.fa-caret-down')).toBeDefined();
- expect(
- component.$el.querySelector('.dropdown-new').getAttribute('data-original-title'),
- ).toEqual('Deploy to...');
+ it('should render a dropdown with the provided list of actions', () => {
+ expect(vm.$el.querySelectorAll('.dropdown-menu li').length).toEqual(actions.length);
+ });
- expect(component.$el.querySelector('.dropdown-new').getAttribute('aria-label')).toEqual(
- 'Deploy to...',
- );
- });
+ it("should render a disabled action when it's not playable", () => {
+ expect(
+ vm.$el.querySelector('.dropdown-menu li:last-child button').getAttribute('disabled'),
+ ).toEqual('disabled');
- it('should render a dropdown with the provided list of actions', () => {
- expect(component.$el.querySelectorAll('.dropdown-menu li').length).toEqual(actionsMock.length);
+ expect(
+ vm.$el.querySelector('.dropdown-menu li:last-child button').classList.contains('disabled'),
+ ).toEqual(true);
+ });
});
- it("should render a disabled action when it's not playable", () => {
- expect(
- component.$el.querySelector('.dropdown-menu li:last-child button').getAttribute('disabled'),
- ).toEqual('disabled');
+ describe('scheduled jobs', () => {
+ const scheduledJobAction = {
+ name: 'scheduled action',
+ playPath: `${TEST_HOST}/scheduled/job/action`,
+ playable: true,
+ scheduledAt: '2063-04-05T00:42:00Z',
+ };
+ const expiredJobAction = {
+ name: 'expired action',
+ playPath: `${TEST_HOST}/expired/job/action`,
+ playable: true,
+ scheduledAt: '2018-10-05T08:23:00Z',
+ };
+ const findDropdownItem = action => {
+ const buttons = vm.$el.querySelectorAll('.dropdown-menu li button');
+ return Array.prototype.find.call(buttons, element =>
+ element.innerText.trim().startsWith(action.name),
+ );
+ };
+
+ beforeEach(() => {
+ spyOn(Date, 'now').and.callFake(() => new Date('2063-04-04T00:42:00Z').getTime());
+ vm = mountComponent(Component, { actions: [scheduledJobAction, expiredJobAction] });
+ });
+
+ it('emits postAction event after confirming', () => {
+ const emitSpy = jasmine.createSpy('emit');
+ eventHub.$on('postAction', emitSpy);
+ spyOn(window, 'confirm').and.callFake(() => true);
+
+ findDropdownItem(scheduledJobAction).click();
+
+ expect(window.confirm).toHaveBeenCalled();
+ expect(emitSpy).toHaveBeenCalledWith({ endpoint: scheduledJobAction.playPath });
+ });
+
+ it('does not emit postAction event if confirmation is cancelled', () => {
+ const emitSpy = jasmine.createSpy('emit');
+ eventHub.$on('postAction', emitSpy);
+ spyOn(window, 'confirm').and.callFake(() => false);
+
+ findDropdownItem(scheduledJobAction).click();
- expect(
- component.$el
- .querySelector('.dropdown-menu li:last-child button')
- .classList.contains('disabled'),
- ).toEqual(true);
+ expect(window.confirm).toHaveBeenCalled();
+ expect(emitSpy).not.toHaveBeenCalled();
+ });
+
+ it('displays the remaining time in the dropdown', () => {
+ expect(findDropdownItem(scheduledJobAction)).toContainText('24:00:00');
+ });
+
+ it('displays 00:00:00 for expired jobs in the dropdown', () => {
+ expect(findDropdownItem(expiredJobAction)).toContainText('00:00:00');
+ });
});
});
diff --git a/spec/javascripts/environments/environment_item_spec.js b/spec/javascripts/environments/environment_item_spec.js
index 0b933dda431..7618c2f50ce 100644
--- a/spec/javascripts/environments/environment_item_spec.js
+++ b/spec/javascripts/environments/environment_item_spec.js
@@ -38,7 +38,9 @@ describe('Environment item', () => {
});
it('Should render the number of children in a badge', () => {
- expect(component.$el.querySelector('.folder-name .badge').textContent).toContain(mockItem.size);
+ expect(component.$el.querySelector('.folder-name .badge').textContent).toContain(
+ mockItem.size,
+ );
});
});
@@ -68,7 +70,8 @@ describe('Environment item', () => {
username: 'root',
id: 1,
state: 'active',
- avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
commit: {
@@ -84,7 +87,8 @@ describe('Environment item', () => {
username: 'root',
id: 1,
state: 'active',
- avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
@@ -121,18 +125,18 @@ describe('Environment item', () => {
});
it('should render environment name', () => {
- expect(component.$el.querySelector('.environment-name').textContent).toContain(environment.name);
+ expect(component.$el.querySelector('.environment-name').textContent).toContain(
+ environment.name,
+ );
});
describe('With deployment', () => {
it('should render deployment internal id', () => {
- expect(
- component.$el.querySelector('.deployment-column span').textContent,
- ).toContain(environment.last_deployment.iid);
+ expect(component.$el.querySelector('.deployment-column span').textContent).toContain(
+ environment.last_deployment.iid,
+ );
- expect(
- component.$el.querySelector('.deployment-column span').textContent,
- ).toContain('#');
+ expect(component.$el.querySelector('.deployment-column span').textContent).toContain('#');
});
it('should render last deployment date', () => {
@@ -156,56 +160,46 @@ describe('Environment item', () => {
describe('With build url', () => {
it('Should link to build url provided', () => {
- expect(
- component.$el.querySelector('.build-link').getAttribute('href'),
- ).toEqual(environment.last_deployment.deployable.build_path);
+ expect(component.$el.querySelector('.build-link').getAttribute('href')).toEqual(
+ environment.last_deployment.deployable.build_path,
+ );
});
it('Should render deployable name and id', () => {
- expect(
- component.$el.querySelector('.build-link').getAttribute('href'),
- ).toEqual(environment.last_deployment.deployable.build_path);
+ expect(component.$el.querySelector('.build-link').getAttribute('href')).toEqual(
+ environment.last_deployment.deployable.build_path,
+ );
});
});
describe('With commit information', () => {
it('should render commit component', () => {
- expect(
- component.$el.querySelector('.js-commit-component'),
- ).toBeDefined();
+ expect(component.$el.querySelector('.js-commit-component')).toBeDefined();
});
});
});
describe('With manual actions', () => {
it('Should render actions component', () => {
- expect(
- component.$el.querySelector('.js-manual-actions-container'),
- ).toBeDefined();
+ expect(component.$el.querySelector('.js-manual-actions-container')).toBeDefined();
});
});
describe('With external URL', () => {
it('should render external url component', () => {
- expect(
- component.$el.querySelector('.js-external-url-container'),
- ).toBeDefined();
+ expect(component.$el.querySelector('.js-external-url-container')).toBeDefined();
});
});
describe('With stop action', () => {
it('Should render stop action component', () => {
- expect(
- component.$el.querySelector('.js-stop-component-container'),
- ).toBeDefined();
+ expect(component.$el.querySelector('.js-stop-component-container')).toBeDefined();
});
});
describe('With retry action', () => {
it('Should render rollback component', () => {
- expect(
- component.$el.querySelector('.js-rollback-component-container'),
- ).toBeDefined();
+ expect(component.$el.querySelector('.js-rollback-component-container')).toBeDefined();
});
});
});
diff --git a/spec/javascripts/environments/environments_app_spec.js b/spec/javascripts/environments/environments_app_spec.js
index 7edc0ccac0b..e2d81eb454a 100644
--- a/spec/javascripts/environments/environments_app_spec.js
+++ b/spec/javascripts/environments/environments_app_spec.js
@@ -31,9 +31,9 @@ describe('Environment', () => {
mock.restore();
});
- describe('successfull request', () => {
+ describe('successful request', () => {
describe('without environments', () => {
- beforeEach((done) => {
+ beforeEach(done => {
mock.onGet(mockData.endpoint).reply(200, { environments: [] });
component = mountComponent(EnvironmentsComponent, mockData);
@@ -44,30 +44,34 @@ describe('Environment', () => {
});
it('should render the empty state', () => {
- expect(
- component.$el.querySelector('.js-new-environment-button').textContent,
- ).toContain('New environment');
+ expect(component.$el.querySelector('.js-new-environment-button').textContent).toContain(
+ 'New environment',
+ );
- expect(
- component.$el.querySelector('.js-blank-state-title').textContent,
- ).toContain('You don\'t have any environments right now');
+ expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain(
+ "You don't have any environments right now",
+ );
});
});
describe('with paginated environments', () => {
- beforeEach((done) => {
- mock.onGet(mockData.endpoint).reply(200, {
- environments: [environment],
- stopped_count: 1,
- available_count: 0,
- }, {
- 'X-nExt-pAge': '2',
- 'x-page': '1',
- 'X-Per-Page': '1',
- 'X-Prev-Page': '',
- 'X-TOTAL': '37',
- 'X-Total-Pages': '2',
- });
+ beforeEach(done => {
+ mock.onGet(mockData.endpoint).reply(
+ 200,
+ {
+ environments: [environment],
+ stopped_count: 1,
+ available_count: 0,
+ },
+ {
+ 'X-nExt-pAge': '2',
+ 'x-page': '1',
+ 'X-Per-Page': '1',
+ 'X-Prev-Page': '',
+ 'X-TOTAL': '37',
+ 'X-Total-Pages': '2',
+ },
+ );
component = mountComponent(EnvironmentsComponent, mockData);
@@ -78,19 +82,17 @@ describe('Environment', () => {
it('should render a table with environments', () => {
expect(component.$el.querySelectorAll('table')).not.toBeNull();
- expect(
- component.$el.querySelector('.environment-name').textContent.trim(),
- ).toEqual(environment.name);
+ expect(component.$el.querySelector('.environment-name').textContent.trim()).toEqual(
+ environment.name,
+ );
});
describe('pagination', () => {
it('should render pagination', () => {
- expect(
- component.$el.querySelectorAll('.gl-pagination li').length,
- ).toEqual(5);
+ expect(component.$el.querySelectorAll('.gl-pagination li').length).toEqual(5);
});
- it('should make an API request when page is clicked', (done) => {
+ it('should make an API request when page is clicked', done => {
spyOn(component, 'updateContent');
setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
@@ -100,7 +102,7 @@ describe('Environment', () => {
}, 0);
});
- it('should make an API request when using tabs', (done) => {
+ it('should make an API request when using tabs', done => {
setTimeout(() => {
spyOn(component, 'updateContent');
component.$el.querySelector('.js-environments-tab-stopped').click();
@@ -114,7 +116,7 @@ describe('Environment', () => {
});
describe('unsuccessfull request', () => {
- beforeEach((done) => {
+ beforeEach(done => {
mock.onGet(mockData.endpoint).reply(500, {});
component = mountComponent(EnvironmentsComponent, mockData);
@@ -125,15 +127,16 @@ describe('Environment', () => {
});
it('should render empty state', () => {
- expect(
- component.$el.querySelector('.js-blank-state-title').textContent,
- ).toContain('You don\'t have any environments right now');
+ expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain(
+ "You don't have any environments right now",
+ );
});
});
describe('expandable folders', () => {
beforeEach(() => {
- mock.onGet(mockData.endpoint).reply(200,
+ mock.onGet(mockData.endpoint).reply(
+ 200,
{
environments: [folder],
stopped_count: 0,
@@ -154,7 +157,7 @@ describe('Environment', () => {
component = mountComponent(EnvironmentsComponent, mockData);
});
- it('should open a closed folder', (done) => {
+ it('should open a closed folder', done => {
setTimeout(() => {
component.$el.querySelector('.folder-name').click();
@@ -165,7 +168,7 @@ describe('Environment', () => {
}, 0);
});
- it('should close an opened folder', (done) => {
+ it('should close an opened folder', done => {
setTimeout(() => {
// open folder
component.$el.querySelector('.folder-name').click();
@@ -182,7 +185,7 @@ describe('Environment', () => {
}, 0);
});
- it('should show children environments and a button to show all environments', (done) => {
+ it('should show children environments and a button to show all environments', done => {
setTimeout(() => {
// open folder
component.$el.querySelector('.folder-name').click();
@@ -191,7 +194,9 @@ describe('Environment', () => {
// wait for next async request
setTimeout(() => {
expect(component.$el.querySelectorAll('.js-child-row').length).toEqual(1);
- expect(component.$el.querySelector('.text-center > a.btn').textContent).toContain('Show all');
+ expect(component.$el.querySelector('.text-center > a.btn').textContent).toContain(
+ 'Show all',
+ );
done();
});
});
@@ -201,7 +206,8 @@ describe('Environment', () => {
describe('methods', () => {
beforeEach(() => {
- mock.onGet(mockData.endpoint).reply(200,
+ mock.onGet(mockData.endpoint).reply(
+ 200,
{
environments: [],
stopped_count: 0,
@@ -215,8 +221,9 @@ describe('Environment', () => {
});
describe('updateContent', () => {
- it('should set given parameters', (done) => {
- component.updateContent({ scope: 'stopped', page: '3' })
+ it('should set given parameters', done => {
+ component
+ .updateContent({ scope: 'stopped', page: '3' })
.then(() => {
expect(component.page).toEqual('3');
expect(component.scope).toEqual('stopped');
diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js
index 51d4213c38f..7f0a9475d5f 100644
--- a/spec/javascripts/environments/folder/environments_folder_view_spec.js
+++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js
@@ -30,39 +30,43 @@ describe('Environments Folder View', () => {
component.$destroy();
});
- describe('successfull request', () => {
+ describe('successful request', () => {
beforeEach(() => {
- mock.onGet(mockData.endpoint).reply(200, {
- environments: environmentsList,
- stopped_count: 1,
- available_count: 0,
- }, {
- 'X-nExt-pAge': '2',
- 'x-page': '1',
- 'X-Per-Page': '2',
- 'X-Prev-Page': '',
- 'X-TOTAL': '20',
- 'X-Total-Pages': '10',
- });
+ mock.onGet(mockData.endpoint).reply(
+ 200,
+ {
+ environments: environmentsList,
+ stopped_count: 1,
+ available_count: 0,
+ },
+ {
+ 'X-nExt-pAge': '2',
+ 'x-page': '1',
+ 'X-Per-Page': '2',
+ 'X-Prev-Page': '',
+ 'X-TOTAL': '20',
+ 'X-Total-Pages': '10',
+ },
+ );
component = mountComponent(Component, mockData);
});
- it('should render a table with environments', (done) => {
+ it('should render a table with environments', done => {
setTimeout(() => {
expect(component.$el.querySelectorAll('table')).not.toBeNull();
- expect(
- component.$el.querySelector('.environment-name').textContent.trim(),
- ).toEqual(environmentsList[0].name);
+ expect(component.$el.querySelector('.environment-name').textContent.trim()).toEqual(
+ environmentsList[0].name,
+ );
done();
}, 0);
});
- it('should render available tab with count', (done) => {
+ it('should render available tab with count', done => {
setTimeout(() => {
- expect(
- component.$el.querySelector('.js-environments-tab-available').textContent,
- ).toContain('Available');
+ expect(component.$el.querySelector('.js-environments-tab-available').textContent).toContain(
+ 'Available',
+ );
expect(
component.$el.querySelector('.js-environments-tab-available .badge').textContent,
@@ -71,11 +75,11 @@ describe('Environments Folder View', () => {
}, 0);
});
- it('should render stopped tab with count', (done) => {
+ it('should render stopped tab with count', done => {
setTimeout(() => {
- expect(
- component.$el.querySelector('.js-environments-tab-stopped').textContent,
- ).toContain('Stopped');
+ expect(component.$el.querySelector('.js-environments-tab-stopped').textContent).toContain(
+ 'Stopped',
+ );
expect(
component.$el.querySelector('.js-environments-tab-stopped .badge').textContent,
@@ -84,36 +88,37 @@ describe('Environments Folder View', () => {
}, 0);
});
- it('should render parent folder name', (done) => {
+ it('should render parent folder name', done => {
setTimeout(() => {
- expect(
- component.$el.querySelector('.js-folder-name').textContent.trim(),
- ).toContain('Environments / review');
+ expect(component.$el.querySelector('.js-folder-name').textContent.trim()).toContain(
+ 'Environments / review',
+ );
done();
}, 0);
});
describe('pagination', () => {
- it('should render pagination', (done) => {
+ it('should render pagination', done => {
setTimeout(() => {
- expect(
- component.$el.querySelectorAll('.gl-pagination'),
- ).not.toBeNull();
+ expect(component.$el.querySelectorAll('.gl-pagination')).not.toBeNull();
done();
}, 0);
});
- it('should make an API request when changing page', (done) => {
+ it('should make an API request when changing page', done => {
spyOn(component, 'updateContent');
setTimeout(() => {
component.$el.querySelector('.gl-pagination .js-last-button a').click();
- expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '10' });
+ expect(component.updateContent).toHaveBeenCalledWith({
+ scope: component.scope,
+ page: '10',
+ });
done();
}, 0);
});
- it('should make an API request when using tabs', (done) => {
+ it('should make an API request when using tabs', done => {
setTimeout(() => {
spyOn(component, 'updateContent');
component.$el.querySelector('.js-environments-tab-stopped').click();
@@ -134,20 +139,18 @@ describe('Environments Folder View', () => {
component = mountComponent(Component, mockData);
});
- it('should not render a table', (done) => {
+ it('should not render a table', done => {
setTimeout(() => {
- expect(
- component.$el.querySelector('table'),
- ).toBe(null);
+ expect(component.$el.querySelector('table')).toBe(null);
done();
}, 0);
});
- it('should render available tab with count 0', (done) => {
+ it('should render available tab with count 0', done => {
setTimeout(() => {
- expect(
- component.$el.querySelector('.js-environments-tab-available').textContent,
- ).toContain('Available');
+ expect(component.$el.querySelector('.js-environments-tab-available').textContent).toContain(
+ 'Available',
+ );
expect(
component.$el.querySelector('.js-environments-tab-available .badge').textContent,
@@ -156,11 +159,11 @@ describe('Environments Folder View', () => {
}, 0);
});
- it('should render stopped tab with count 0', (done) => {
+ it('should render stopped tab with count 0', done => {
setTimeout(() => {
- expect(
- component.$el.querySelector('.js-environments-tab-stopped').textContent,
- ).toContain('Stopped');
+ expect(component.$el.querySelector('.js-environments-tab-stopped').textContent).toContain(
+ 'Stopped',
+ );
expect(
component.$el.querySelector('.js-environments-tab-stopped .badge').textContent,
@@ -181,8 +184,9 @@ describe('Environments Folder View', () => {
});
describe('updateContent', () => {
- it('should set given parameters', (done) => {
- component.updateContent({ scope: 'stopped', page: '4' })
+ it('should set given parameters', done => {
+ component
+ .updateContent({ scope: 'stopped', page: '4' })
.then(() => {
expect(component.page).toEqual('4');
expect(component.scope).toEqual('stopped');
diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
index 0c1d5f5b0b4..4f561df7943 100644
--- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
@@ -754,6 +754,50 @@ describe('Filtered Search Visual Tokens', () => {
expect(updateLabelTokenColorSpy.calls.count()).toBe(0);
expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0);
});
+
+ it('does not update user token appearance for `none` filter', () => {
+ const { tokenNameElement } = findElements(authorToken);
+
+ const tokenName = tokenNameElement.innerText;
+ const tokenValue = 'none';
+
+ subject.renderVisualTokenValue(authorToken, tokenName, tokenValue);
+
+ expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0);
+ });
+
+ it('does not update user token appearance for `any` filter', () => {
+ const { tokenNameElement } = findElements(authorToken);
+
+ const tokenName = tokenNameElement.innerText;
+ const tokenValue = 'any';
+
+ subject.renderVisualTokenValue(authorToken, tokenName, tokenValue);
+
+ expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0);
+ });
+
+ it('does not update label token color for `none` filter', () => {
+ const { tokenNameElement } = findElements(bugLabelToken);
+
+ const tokenName = tokenNameElement.innerText;
+ const tokenValue = 'none';
+
+ subject.renderVisualTokenValue(bugLabelToken, tokenName, tokenValue);
+
+ expect(updateLabelTokenColorSpy.calls.count()).toBe(0);
+ });
+
+ it('does not update label token color for `any` filter', () => {
+ const { tokenNameElement } = findElements(bugLabelToken);
+
+ const tokenName = tokenNameElement.innerText;
+ const tokenValue = 'any';
+
+ subject.renderVisualTokenValue(bugLabelToken, tokenName, tokenValue);
+
+ expect(updateLabelTokenColorSpy.calls.count()).toBe(0);
+ });
});
describe('updateUserTokenAppearance', () => {
@@ -763,19 +807,6 @@ describe('Filtered Search Visual Tokens', () => {
spyOn(UsersCache, 'retrieve').and.callFake(username => usersCacheSpy(username));
});
- it('ignores special value "none"', done => {
- usersCacheSpy = username => {
- expect(username).toBe('none');
- done.fail('Should not resolve "none"!');
- };
- const { tokenValueContainer, tokenValueElement } = findElements(authorToken);
-
- subject
- .updateUserTokenAppearance(tokenValueContainer, tokenValueElement, 'none')
- .then(done)
- .catch(done.fail);
- });
-
it('ignores error if UsersCache throws', done => {
spyOn(window, 'Flash');
const dummyError = new Error('Earth rotated backwards');
diff --git a/spec/javascripts/fixtures/jobs.rb b/spec/javascripts/fixtures/jobs.rb
index 6d5c6d5334f..82d7a5e394e 100644
--- a/spec/javascripts/fixtures/jobs.rb
+++ b/spec/javascripts/fixtures/jobs.rb
@@ -5,16 +5,24 @@ describe Projects::JobsController, '(JavaScript fixtures)', type: :controller do
let(:admin) { create(:admin) }
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
- let(:project) { create(:project_empty_repo, namespace: namespace, path: 'builds-project') }
- let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+ let(:project) { create(:project, :repository, namespace: namespace, path: 'builds-project') }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id) }
let!(:build_with_artifacts) { create(:ci_build, :success, :artifacts, :trace_artifact, pipeline: pipeline, stage: 'test', artifacts_expire_at: Time.now + 18.months) }
let!(:failed_build) { create(:ci_build, :failed, pipeline: pipeline, stage: 'build') }
let!(:pending_build) { create(:ci_build, :pending, pipeline: pipeline, stage: 'deploy') }
+ let!(:delayed_job) do
+ create(:ci_build, :scheduled,
+ pipeline: pipeline,
+ name: 'delayed job',
+ stage: 'test',
+ commands: 'test')
+ end
render_views
before(:all) do
clean_frontend_fixtures('builds/')
+ clean_frontend_fixtures('jobs/')
end
before do
@@ -34,4 +42,15 @@ describe Projects::JobsController, '(JavaScript fixtures)', type: :controller do
expect(response).to be_success
store_frontend_fixture(response, example.description)
end
+
+ it 'jobs/delayed.json' do |example|
+ get :show,
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: delayed_job.to_param,
+ format: :json
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
end
diff --git a/spec/javascripts/issue_show/components/title_spec.js b/spec/javascripts/issue_show/components/title_spec.js
index 9e6f236b687..9754c8a6755 100644
--- a/spec/javascripts/issue_show/components/title_spec.js
+++ b/spec/javascripts/issue_show/components/title_spec.js
@@ -25,25 +25,21 @@ describe('Title component', () => {
});
it('renders title HTML', () => {
- expect(
- vm.$el.querySelector('.title').innerHTML.trim(),
- ).toBe('Testing <img>');
+ expect(vm.$el.querySelector('.title').innerHTML.trim()).toBe('Testing <img>');
});
- it('updates page title when changing titleHtml', (done) => {
+ it('updates page title when changing titleHtml', done => {
spyOn(vm, 'setPageTitle');
vm.titleHtml = 'test';
Vue.nextTick(() => {
- expect(
- vm.setPageTitle,
- ).toHaveBeenCalled();
+ expect(vm.setPageTitle).toHaveBeenCalled();
done();
});
});
- it('animates title changes', (done) => {
+ it('animates title changes', done => {
vm.titleHtml = 'test';
Vue.nextTick(() => {
@@ -61,14 +57,12 @@ describe('Title component', () => {
});
});
- it('updates page title after changing title', (done) => {
+ it('updates page title after changing title', done => {
vm.titleHtml = 'changed';
vm.titleText = 'changed';
Vue.nextTick(() => {
- expect(
- document.querySelector('title').textContent.trim(),
- ).toContain('changed');
+ expect(document.querySelector('title').textContent.trim()).toContain('changed');
done();
});
diff --git a/spec/javascripts/jobs/components/job_app_spec.js b/spec/javascripts/jobs/components/job_app_spec.js
index 288c06d6615..fcf3780f0ea 100644
--- a/spec/javascripts/jobs/components/job_app_spec.js
+++ b/spec/javascripts/jobs/components/job_app_spec.js
@@ -8,6 +8,7 @@ import { resetStore } from '../store/helpers';
import job from '../mock_data';
describe('Job App ', () => {
+ const delayedJobFixture = getJSONFixture('jobs/delayed.json');
const Component = Vue.extend(jobApp);
let store;
let vm;
@@ -52,7 +53,7 @@ describe('Job App ', () => {
});
});
- describe('with successfull request', () => {
+ describe('with successful request', () => {
beforeEach(() => {
mock.onGet(`${props.pagePath}/trace.json`).replyOnce(200, {});
});
@@ -101,7 +102,7 @@ describe('Job App ', () => {
.querySelector('.header-main-content')
.textContent.replace(/\s+/g, ' ')
.trim(),
- ).toEqual('passed Job #4757 triggered 1 year ago by Root');
+ ).toContain('passed Job #4757 triggered 1 year ago by Root');
done();
}, 0);
});
@@ -127,7 +128,7 @@ describe('Job App ', () => {
.querySelector('.header-main-content')
.textContent.replace(/\s+/g, ' ')
.trim(),
- ).toEqual('passed Job #4757 created 3 weeks ago by Root');
+ ).toContain('passed Job #4757 created 3 weeks ago by Root');
done();
}, 0);
});
@@ -234,7 +235,7 @@ describe('Job App ', () => {
);
done();
}, 0);
- })
+ });
});
it('does not renders stuck block when there are no runners', done => {
@@ -420,6 +421,70 @@ describe('Job App ', () => {
done();
}, 0);
});
+
+ it('displays remaining time for a delayed job', done => {
+ const oneHourInMilliseconds = 3600000;
+ spyOn(Date, 'now').and.callFake(
+ () => new Date(delayedJobFixture.scheduled_at).getTime() - oneHourInMilliseconds,
+ );
+ mock.onGet(props.endpoint).replyOnce(200, { ...delayedJobFixture });
+
+ vm = mountComponentWithStore(Component, {
+ props,
+ store,
+ });
+
+ store.subscribeAction(action => {
+ if (action.type !== 'receiveJobSuccess') {
+ return;
+ }
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.js-job-empty-state')).not.toBeNull();
+
+ const title = vm.$el.querySelector('.js-job-empty-state-title');
+
+ expect(title).toContainText('01:00:00');
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+ });
+ });
+
+ describe('archived job', () => {
+ beforeEach(() => {
+ mock.onGet(props.endpoint).reply(200, Object.assign({}, job, { archived: true }), {});
+ vm = mountComponentWithStore(Component, {
+ props,
+ store,
+ });
+ });
+
+ it('renders warning about job being archived', done => {
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.js-archived-job ')).not.toBeNull();
+ done();
+ }, 0);
+ });
+ });
+
+ describe('non-archived job', () => {
+ beforeEach(() => {
+ mock.onGet(props.endpoint).reply(200, job, {});
+ vm = mountComponentWithStore(Component, {
+ props,
+ store,
+ });
+ });
+
+ it('does not warning about job being archived', done => {
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.js-archived-job ')).toBeNull();
+ done();
+ }, 0);
});
});
diff --git a/spec/javascripts/jobs/components/job_container_item_spec.js b/spec/javascripts/jobs/components/job_container_item_spec.js
index 8588eda19c8..2d108f1ad7f 100644
--- a/spec/javascripts/jobs/components/job_container_item_spec.js
+++ b/spec/javascripts/jobs/components/job_container_item_spec.js
@@ -4,6 +4,7 @@ import mountComponent from 'spec/helpers/vue_mount_component_helper';
import job from '../mock_data';
describe('JobContainerItem', () => {
+ const delayedJobFixture = getJSONFixture('jobs/delayed.json');
const Component = Vue.extend(JobContainerItem);
let vm;
@@ -70,4 +71,29 @@ describe('JobContainerItem', () => {
expect(vm.$el).toHaveSpriteIcon('retry');
});
});
+
+ describe('for delayed job', () => {
+ beforeEach(() => {
+ const remainingMilliseconds = 1337000;
+ spyOn(Date, 'now').and.callFake(
+ () => new Date(delayedJobFixture.scheduled_at).getTime() - remainingMilliseconds,
+ );
+ });
+
+ it('displays remaining time in tooltip', done => {
+ vm = mountComponent(Component, {
+ job: delayedJobFixture,
+ isActive: false,
+ });
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.js-job-link').getAttribute('data-original-title')).toEqual(
+ 'delayed job - delayed manual action (00:22:17)',
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
});
diff --git a/spec/javascripts/jobs/mixins/delayed_job_mixin_spec.js b/spec/javascripts/jobs/mixins/delayed_job_mixin_spec.js
new file mode 100644
index 00000000000..48a6b80b365
--- /dev/null
+++ b/spec/javascripts/jobs/mixins/delayed_job_mixin_spec.js
@@ -0,0 +1,93 @@
+import Vue from 'vue';
+import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('DelayedJobMixin', () => {
+ const delayedJobFixture = getJSONFixture('jobs/delayed.json');
+ const dummyComponent = Vue.extend({
+ mixins: [delayedJobMixin],
+ props: {
+ job: {
+ type: Object,
+ required: true,
+ },
+ },
+ template: '<div>{{ remainingTime }}</div>',
+ });
+
+ let vm;
+
+ beforeEach(() => {
+ jasmine.clock().install();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ jasmine.clock().uninstall();
+ });
+
+ describe('if job is empty object', () => {
+ beforeEach(() => {
+ vm = mountComponent(dummyComponent, {
+ job: {},
+ });
+ });
+
+ it('sets remaining time to 00:00:00', () => {
+ expect(vm.$el.innerText).toBe('00:00:00');
+ });
+
+ describe('after mounting', () => {
+ beforeEach(done => {
+ Vue.nextTick()
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('doe not update remaining time', () => {
+ expect(vm.$el.innerText).toBe('00:00:00');
+ });
+ });
+ });
+
+ describe('if job is delayed job', () => {
+ let remainingTimeInMilliseconds = 42000;
+
+ beforeEach(() => {
+ spyOn(Date, 'now').and.callFake(
+ () => new Date(delayedJobFixture.scheduled_at).getTime() - remainingTimeInMilliseconds,
+ );
+ vm = mountComponent(dummyComponent, {
+ job: delayedJobFixture,
+ });
+ });
+
+ it('sets remaining time to 00:00:00', () => {
+ expect(vm.$el.innerText).toBe('00:00:00');
+ });
+
+ describe('after mounting', () => {
+ beforeEach(done => {
+ Vue.nextTick()
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('sets remaining time', () => {
+ expect(vm.$el.innerText).toBe('00:00:42');
+ });
+
+ it('updates remaining time', done => {
+ remainingTimeInMilliseconds = 41000;
+ jasmine.clock().tick(1000);
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.$el.innerText).toBe('00:00:41');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/jobs/store/actions_spec.js b/spec/javascripts/jobs/store/actions_spec.js
index 45130b983e7..77b44995b12 100644
--- a/spec/javascripts/jobs/store/actions_spec.js
+++ b/spec/javascripts/jobs/store/actions_spec.js
@@ -68,41 +68,20 @@ describe('Job State actions', () => {
describe('hideSidebar', () => {
it('should commit HIDE_SIDEBAR mutation', done => {
- testAction(
- hideSidebar,
- null,
- mockedState,
- [{ type: types.HIDE_SIDEBAR }],
- [],
- done,
- );
+ testAction(hideSidebar, null, mockedState, [{ type: types.HIDE_SIDEBAR }], [], done);
});
});
describe('showSidebar', () => {
it('should commit HIDE_SIDEBAR mutation', done => {
- testAction(
- showSidebar,
- null,
- mockedState,
- [{ type: types.SHOW_SIDEBAR }],
- [],
- done,
- );
+ testAction(showSidebar, null, mockedState, [{ type: types.SHOW_SIDEBAR }], [], done);
});
});
describe('toggleSidebar', () => {
describe('when isSidebarOpen is true', () => {
it('should dispatch hideSidebar', done => {
- testAction(
- toggleSidebar,
- null,
- mockedState,
- [],
- [{ type: 'hideSidebar' }],
- done,
- );
+ testAction(toggleSidebar, null, mockedState, [], [{ type: 'hideSidebar' }], done);
});
});
@@ -110,14 +89,7 @@ describe('Job State actions', () => {
it('should dispatch showSidebar', done => {
mockedState.isSidebarOpen = false;
- testAction(
- toggleSidebar,
- null,
- mockedState,
- [],
- [{ type: 'showSidebar' }],
- done,
- );
+ testAction(toggleSidebar, null, mockedState, [], [{ type: 'showSidebar' }], done);
});
});
});
diff --git a/spec/javascripts/jobs/store/getters_spec.js b/spec/javascripts/jobs/store/getters_spec.js
index 34e9707eadd..4195d9d3680 100644
--- a/spec/javascripts/jobs/store/getters_spec.js
+++ b/spec/javascripts/jobs/store/getters_spec.js
@@ -180,7 +180,7 @@ describe('Job Store Getters', () => {
it('returns true', () => {
localState.job.runners = {
available: true,
- online: false
+ online: false,
};
expect(getters.hasRunnersForProject(localState)).toEqual(true);
@@ -191,7 +191,7 @@ describe('Job Store Getters', () => {
it('returns false', () => {
localState.job.runners = {
available: false,
- online: false
+ online: false,
};
expect(getters.hasRunnersForProject(localState)).toEqual(false);
@@ -202,7 +202,7 @@ describe('Job Store Getters', () => {
it('returns false', () => {
localState.job.runners = {
available: false,
- online: true
+ online: true,
};
expect(getters.hasRunnersForProject(localState)).toEqual(false);
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js
index 514d6ddeae5..0fb90c3b78c 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js
+++ b/spec/javascripts/lib/utils/common_utils_spec.js
@@ -35,9 +35,7 @@ describe('common_utils', () => {
});
it('should decode params', () => {
- expect(
- commonUtils.urlParamsToArray('?label_name%5B%5D=test')[0],
- ).toBe('label_name[]=test');
+ expect(commonUtils.urlParamsToArray('?label_name%5B%5D=test')[0]).toBe('label_name[]=test');
});
it('should remove the question mark from the search params', () => {
@@ -49,25 +47,19 @@ describe('common_utils', () => {
describe('urlParamsToObject', () => {
it('parses path for label with trailing +', () => {
- expect(
- commonUtils.urlParamsToObject('label_name[]=label%2B', {}),
- ).toEqual({
+ expect(commonUtils.urlParamsToObject('label_name[]=label%2B', {})).toEqual({
label_name: ['label+'],
});
});
it('parses path for milestone with trailing +', () => {
- expect(
- commonUtils.urlParamsToObject('milestone_title=A%2B', {}),
- ).toEqual({
+ expect(commonUtils.urlParamsToObject('milestone_title=A%2B', {})).toEqual({
milestone_title: 'A+',
});
});
it('parses path for search terms with spaces', () => {
- expect(
- commonUtils.urlParamsToObject('search=two+words', {}),
- ).toEqual({
+ expect(commonUtils.urlParamsToObject('search=two+words', {})).toEqual({
search: 'two words',
});
});
@@ -187,7 +179,11 @@ describe('common_utils', () => {
describe('parseQueryStringIntoObject', () => {
it('should return object with query parameters', () => {
- expect(commonUtils.parseQueryStringIntoObject('scope=all&page=2')).toEqual({ scope: 'all', page: '2' });
+ expect(commonUtils.parseQueryStringIntoObject('scope=all&page=2')).toEqual({
+ scope: 'all',
+ page: '2',
+ });
+
expect(commonUtils.parseQueryStringIntoObject('scope=all')).toEqual({ scope: 'all' });
expect(commonUtils.parseQueryStringIntoObject()).toEqual({});
});
@@ -211,7 +207,9 @@ describe('common_utils', () => {
describe('buildUrlWithCurrentLocation', () => {
it('should build an url with current location and given parameters', () => {
expect(commonUtils.buildUrlWithCurrentLocation()).toEqual(window.location.pathname);
- expect(commonUtils.buildUrlWithCurrentLocation('?page=2')).toEqual(`${window.location.pathname}?page=2`);
+ expect(commonUtils.buildUrlWithCurrentLocation('?page=2')).toEqual(
+ `${window.location.pathname}?page=2`,
+ );
});
});
@@ -266,21 +264,24 @@ describe('common_utils', () => {
});
describe('normalizeCRLFHeaders', () => {
- beforeEach(function () {
- this.CLRFHeaders = 'a-header: a-value\nAnother-Header: ANOTHER-VALUE\nLaSt-HeAdEr: last-VALUE';
+ beforeEach(function() {
+ this.CLRFHeaders =
+ 'a-header: a-value\nAnother-Header: ANOTHER-VALUE\nLaSt-HeAdEr: last-VALUE';
spyOn(String.prototype, 'split').and.callThrough();
this.normalizeCRLFHeaders = commonUtils.normalizeCRLFHeaders(this.CLRFHeaders);
});
- it('should split by newline', function () {
+ it('should split by newline', function() {
expect(String.prototype.split).toHaveBeenCalledWith('\n');
});
- it('should split by colon+space for each header', function () {
- expect(String.prototype.split.calls.allArgs().filter(args => args[0] === ': ').length).toBe(3);
+ it('should split by colon+space for each header', function() {
+ expect(String.prototype.split.calls.allArgs().filter(args => args[0] === ': ').length).toBe(
+ 3,
+ );
});
- it('should return a normalized headers object', function () {
+ it('should return a normalized headers object', function() {
expect(this.normalizeCRLFHeaders).toEqual({
'A-HEADER': 'a-value',
'ANOTHER-HEADER': 'ANOTHER-VALUE',
@@ -359,67 +360,79 @@ describe('common_utils', () => {
spyOn(window, 'setTimeout').and.callFake(cb => origSetTimeout(cb, 0));
});
- it('solves the promise from the callback', (done) => {
+ it('solves the promise from the callback', done => {
const expectedResponseValue = 'Success!';
- commonUtils.backOff((next, stop) => (
- new Promise((resolve) => {
- resolve(expectedResponseValue);
- }).then((resp) => {
- stop(resp);
+ commonUtils
+ .backOff((next, stop) =>
+ new Promise(resolve => {
+ resolve(expectedResponseValue);
+ })
+ .then(resp => {
+ stop(resp);
+ })
+ .catch(done.fail),
+ )
+ .then(respBackoff => {
+ expect(respBackoff).toBe(expectedResponseValue);
+ done();
})
- ).catch(done.fail)).then((respBackoff) => {
- expect(respBackoff).toBe(expectedResponseValue);
- done();
- }).catch(done.fail);
+ .catch(done.fail);
});
- it('catches the rejected promise from the callback ', (done) => {
+ it('catches the rejected promise from the callback ', done => {
const errorMessage = 'Mistakes were made!';
- commonUtils.backOff((next, stop) => {
- new Promise((resolve, reject) => {
- reject(new Error(errorMessage));
- }).then((resp) => {
- stop(resp);
- }).catch(err => stop(err));
- }).catch((errBackoffResp) => {
- expect(errBackoffResp instanceof Error).toBe(true);
- expect(errBackoffResp.message).toBe(errorMessage);
- done();
- });
+ commonUtils
+ .backOff((next, stop) => {
+ new Promise((resolve, reject) => {
+ reject(new Error(errorMessage));
+ })
+ .then(resp => {
+ stop(resp);
+ })
+ .catch(err => stop(err));
+ })
+ .catch(errBackoffResp => {
+ expect(errBackoffResp instanceof Error).toBe(true);
+ expect(errBackoffResp.message).toBe(errorMessage);
+ done();
+ });
});
- it('solves the promise correctly after retrying a third time', (done) => {
+ it('solves the promise correctly after retrying a third time', done => {
let numberOfCalls = 1;
const expectedResponseValue = 'Success!';
- commonUtils.backOff((next, stop) => (
- Promise.resolve(expectedResponseValue)
- .then((resp) => {
- if (numberOfCalls < 3) {
- numberOfCalls += 1;
- next();
- } else {
- stop(resp);
- }
- })
- ).catch(done.fail)).then((respBackoff) => {
- const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout);
+ commonUtils
+ .backOff((next, stop) =>
+ Promise.resolve(expectedResponseValue)
+ .then(resp => {
+ if (numberOfCalls < 3) {
+ numberOfCalls += 1;
+ next();
+ } else {
+ stop(resp);
+ }
+ })
+ .catch(done.fail),
+ )
+ .then(respBackoff => {
+ const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout);
- expect(timeouts).toEqual([2000, 4000]);
- expect(respBackoff).toBe(expectedResponseValue);
- done();
- }).catch(done.fail);
+ expect(timeouts).toEqual([2000, 4000]);
+ expect(respBackoff).toBe(expectedResponseValue);
+ done();
+ })
+ .catch(done.fail);
});
- it('rejects the backOff promise after timing out', (done) => {
- commonUtils.backOff(next => next(), 64000)
- .catch((errBackoffResp) => {
- const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout);
+ it('rejects the backOff promise after timing out', done => {
+ commonUtils.backOff(next => next(), 64000).catch(errBackoffResp => {
+ const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout);
- expect(timeouts).toEqual([2000, 4000, 8000, 16000, 32000, 32000]);
- expect(errBackoffResp instanceof Error).toBe(true);
- expect(errBackoffResp.message).toBe('BACKOFF_TIMEOUT');
- done();
- });
+ expect(timeouts).toEqual([2000, 4000, 8000, 16000, 32000, 32000]);
+ expect(errBackoffResp instanceof Error).toBe(true);
+ expect(errBackoffResp.message).toBe('BACKOFF_TIMEOUT');
+ done();
+ });
});
});
@@ -466,11 +479,14 @@ describe('common_utils', () => {
});
describe('createOverlayIcon', () => {
- it('should return the favicon with the overlay', (done) => {
- commonUtils.createOverlayIcon(faviconDataUrl, overlayDataUrl).then((url) => {
- expect(url).toEqual(faviconWithOverlayDataUrl);
- done();
- }).catch(done.fail);
+ it('should return the favicon with the overlay', done => {
+ commonUtils
+ .createOverlayIcon(faviconDataUrl, overlayDataUrl)
+ .then(url => {
+ expect(url).toEqual(faviconWithOverlayDataUrl);
+ done();
+ })
+ .catch(done.fail);
});
});
@@ -486,11 +502,16 @@ describe('common_utils', () => {
document.body.removeChild(document.getElementById('favicon'));
});
- it('should set page favicon to provided favicon overlay', (done) => {
- commonUtils.setFaviconOverlay(overlayDataUrl).then(() => {
- expect(document.getElementById('favicon').getAttribute('href')).toEqual(faviconWithOverlayDataUrl);
- done();
- }).catch(done.fail);
+ it('should set page favicon to provided favicon overlay', done => {
+ commonUtils
+ .setFaviconOverlay(overlayDataUrl)
+ .then(() => {
+ expect(document.getElementById('favicon').getAttribute('href')).toEqual(
+ faviconWithOverlayDataUrl,
+ );
+ done();
+ })
+ .catch(done.fail);
});
});
@@ -512,24 +533,24 @@ describe('common_utils', () => {
document.body.removeChild(document.getElementById('favicon'));
});
- it('should reset favicon in case of error', (done) => {
+ it('should reset favicon in case of error', done => {
mock.onGet(BUILD_URL).replyOnce(500);
- commonUtils.setCiStatusFavicon(BUILD_URL)
- .catch(() => {
- const favicon = document.getElementById('favicon');
+ commonUtils.setCiStatusFavicon(BUILD_URL).catch(() => {
+ const favicon = document.getElementById('favicon');
- expect(favicon.getAttribute('href')).toEqual(faviconDataUrl);
- done();
- });
+ expect(favicon.getAttribute('href')).toEqual(faviconDataUrl);
+ done();
+ });
});
- it('should set page favicon to CI status favicon based on provided status', (done) => {
+ it('should set page favicon to CI status favicon based on provided status', done => {
mock.onGet(BUILD_URL).reply(200, {
favicon: overlayDataUrl,
});
- commonUtils.setCiStatusFavicon(BUILD_URL)
+ commonUtils
+ .setCiStatusFavicon(BUILD_URL)
.then(() => {
const favicon = document.getElementById('favicon');
@@ -554,11 +575,15 @@ describe('common_utils', () => {
});
it('should return the svg for a linked icon', () => {
- expect(commonUtils.spriteIcon('test')).toEqual('<svg ><use xlink:href="icons.svg#test" /></svg>');
+ expect(commonUtils.spriteIcon('test')).toEqual(
+ '<svg ><use xlink:href="icons.svg#test" /></svg>',
+ );
});
it('should set svg className when passed', () => {
- expect(commonUtils.spriteIcon('test', 'fa fa-test')).toEqual('<svg class="fa fa-test"><use xlink:href="icons.svg#test" /></svg>');
+ expect(commonUtils.spriteIcon('test', 'fa fa-test')).toEqual(
+ '<svg class="fa fa-test"><use xlink:href="icons.svg#test" /></svg>',
+ );
});
});
@@ -578,7 +603,7 @@ describe('common_utils', () => {
const convertedObj = commonUtils.convertObjectPropsToCamelCase(mockObj);
- Object.keys(convertedObj).forEach((prop) => {
+ Object.keys(convertedObj).forEach(prop => {
expect(snakeRegEx.test(prop)).toBeFalsy();
expect(convertedObj[prop]).toBe(mockObj[mappings[prop]]);
});
@@ -597,9 +622,7 @@ describe('common_utils', () => {
},
};
- expect(
- commonUtils.convertObjectPropsToCamelCase(obj),
- ).toEqual({
+ expect(commonUtils.convertObjectPropsToCamelCase(obj)).toEqual({
snakeKey: {
child_snake_key: 'value',
},
@@ -614,9 +637,7 @@ describe('common_utils', () => {
},
};
- expect(
- commonUtils.convertObjectPropsToCamelCase(obj, { deep: true }),
- ).toEqual({
+ expect(commonUtils.convertObjectPropsToCamelCase(obj, { deep: true })).toEqual({
snakeKey: {
childSnakeKey: 'value',
},
@@ -630,9 +651,7 @@ describe('common_utils', () => {
},
];
- expect(
- commonUtils.convertObjectPropsToCamelCase(arr, { deep: true }),
- ).toEqual([
+ expect(commonUtils.convertObjectPropsToCamelCase(arr, { deep: true })).toEqual([
{
childSnakeKey: 'value',
},
@@ -648,9 +667,7 @@ describe('common_utils', () => {
],
];
- expect(
- commonUtils.convertObjectPropsToCamelCase(arr, { deep: true }),
- ).toEqual([
+ expect(commonUtils.convertObjectPropsToCamelCase(arr, { deep: true })).toEqual([
[
{
childSnakeKey: 'value',
diff --git a/spec/javascripts/lib/utils/datetime_utility_spec.js b/spec/javascripts/lib/utils/datetime_utility_spec.js
index de6b96aab57..bebe76f76c5 100644
--- a/spec/javascripts/lib/utils/datetime_utility_spec.js
+++ b/spec/javascripts/lib/utils/datetime_utility_spec.js
@@ -199,11 +199,11 @@ describe('datefix', () => {
expect(datetimeUtility.pad(2)).toEqual('02');
});
- it('should not add a zero when lenght matches the default', () => {
+ it('should not add a zero when length matches the default', () => {
expect(datetimeUtility.pad(12)).toEqual('12');
});
- it('should add a 0 when lenght is smaller than the provided', () => {
+ it('should add a 0 when length is smaller than the provided', () => {
expect(datetimeUtility.pad(12, 3)).toEqual('012');
});
});
@@ -336,6 +336,12 @@ describe('prettyTime methods', () => {
expect(timeString).toBe('0m');
});
+
+ it('should return non-condensed representation of time object', () => {
+ const timeObject = { weeks: 1, days: 0, hours: 1, minutes: 0 };
+
+ expect(datetimeUtility.stringifyTime(timeObject, true)).toEqual('1 week 1 hour');
+ });
});
describe('abbreviateTime', () => {
diff --git a/spec/javascripts/lib/utils/number_utility_spec.js b/spec/javascripts/lib/utils/number_utility_spec.js
index a5099a2a3b8..94c6214c86a 100644
--- a/spec/javascripts/lib/utils/number_utility_spec.js
+++ b/spec/javascripts/lib/utils/number_utility_spec.js
@@ -1,4 +1,10 @@
-import { formatRelevantDigits, bytesToKiB, bytesToMiB, bytesToGiB, numberToHumanSize } from '~/lib/utils/number_utils';
+import {
+ formatRelevantDigits,
+ bytesToKiB,
+ bytesToMiB,
+ bytesToGiB,
+ numberToHumanSize,
+} from '~/lib/utils/number_utils';
describe('Number Utils', () => {
describe('formatRelevantDigits', () => {
diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js
index ac3270baef5..92ebfc38722 100644
--- a/spec/javascripts/lib/utils/text_utility_spec.js
+++ b/spec/javascripts/lib/utils/text_utility_spec.js
@@ -120,7 +120,7 @@ describe('text_utility', () => {
});
describe('getFirstCharacterCapitalized', () => {
- it('returns the first character captialized, if first character is alphabetic', () => {
+ it('returns the first character capitalized, if first character is alphabetic', () => {
expect(textUtils.getFirstCharacterCapitalized('loremIpsumDolar')).toEqual('L');
expect(textUtils.getFirstCharacterCapitalized('Sit amit !')).toEqual('S');
});
diff --git a/spec/javascripts/monitoring/graph/flag_spec.js b/spec/javascripts/monitoring/graph/flag_spec.js
index a837b71db0b..038bfffd44f 100644
--- a/spec/javascripts/monitoring/graph/flag_spec.js
+++ b/spec/javascripts/monitoring/graph/flag_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import GraphFlag from '~/monitoring/components/graph/flag.vue';
import { deploymentData } from '../mock_data';
-const createComponent = (propsData) => {
+const createComponent = propsData => {
const Component = Vue.extend(GraphFlag);
return new Component({
@@ -51,8 +51,7 @@ describe('GraphFlag', () => {
it('has a line at the currentXCoordinate', () => {
component = createComponent(defaultValuesComponent);
- expect(component.$el.style.left)
- .toEqual(`${70 + component.currentXCoordinate}px`);
+ expect(component.$el.style.left).toEqual(`${70 + component.currentXCoordinate}px`);
});
describe('Deployment flag', () => {
@@ -62,9 +61,7 @@ describe('GraphFlag', () => {
deploymentFlagData,
});
- expect(
- deploymentFlagComponent.$el.querySelector('.popover-title'),
- ).toContainText('Deployed');
+ expect(deploymentFlagComponent.$el.querySelector('.popover-title')).toContainText('Deployed');
});
it('contains the ref when a tag is available', () => {
@@ -78,13 +75,13 @@ describe('GraphFlag', () => {
},
});
- expect(
- deploymentFlagComponent.$el.querySelector('.deploy-meta-content'),
- ).toContainText('f5bcd1d9');
+ expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).toContainText(
+ 'f5bcd1d9',
+ );
- expect(
- deploymentFlagComponent.$el.querySelector('.deploy-meta-content'),
- ).toContainText('1.0');
+ expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).toContainText(
+ '1.0',
+ );
});
it('does not contain the ref when a tag is unavailable', () => {
@@ -98,13 +95,13 @@ describe('GraphFlag', () => {
},
});
- expect(
- deploymentFlagComponent.$el.querySelector('.deploy-meta-content'),
- ).toContainText('f5bcd1d9');
+ expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).toContainText(
+ 'f5bcd1d9',
+ );
- expect(
- deploymentFlagComponent.$el.querySelector('.deploy-meta-content'),
- ).not.toContainText('1.0');
+ expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).not.toContainText(
+ '1.0',
+ );
});
});
diff --git a/spec/javascripts/notes/components/diff_with_note_spec.js b/spec/javascripts/notes/components/diff_with_note_spec.js
index 239d7950907..95461396f10 100644
--- a/spec/javascripts/notes/components/diff_with_note_spec.js
+++ b/spec/javascripts/notes/components/diff_with_note_spec.js
@@ -1,7 +1,6 @@
import Vue from 'vue';
import DiffWithNote from '~/notes/components/diff_with_note.vue';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import createStore from '~/notes/stores';
+import { createStore } from '~/mr_notes/stores';
import { mountComponentWithStore } from 'spec/helpers';
const discussionFixture = 'merge_requests/diff_discussion.json';
@@ -11,7 +10,7 @@ describe('diff_with_note', () => {
let store;
let vm;
const diffDiscussionMock = getJSONFixture(discussionFixture)[0];
- const diffDiscussion = convertObjectPropsToCamelCase(diffDiscussionMock);
+ const diffDiscussion = diffDiscussionMock;
const Component = Vue.extend(DiffWithNote);
const props = {
discussion: diffDiscussion,
@@ -65,7 +64,7 @@ describe('diff_with_note', () => {
describe('image diff', () => {
beforeEach(() => {
const imageDiffDiscussionMock = getJSONFixture(imageDiscussionFixture)[0];
- props.discussion = convertObjectPropsToCamelCase(imageDiffDiscussionMock);
+ props.discussion = imageDiffDiscussionMock;
});
it('shows image diff', () => {
diff --git a/spec/javascripts/notes/components/discussion_filter_spec.js b/spec/javascripts/notes/components/discussion_filter_spec.js
index 70dd5bb3be5..9070d968cfd 100644
--- a/spec/javascripts/notes/components/discussion_filter_spec.js
+++ b/spec/javascripts/notes/components/discussion_filter_spec.js
@@ -11,13 +11,15 @@ describe('DiscussionFilter component', () => {
beforeEach(() => {
store = createStore();
- const discussions = [{
- ...discussionMock,
- id: discussionMock.id,
- notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: true }],
- }];
+ const discussions = [
+ {
+ ...discussionMock,
+ id: discussionMock.id,
+ notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: true }],
+ },
+ ];
const Component = Vue.extend(DiscussionFilter);
- const defaultValue = discussionFiltersMock[0].value;
+ const selectedValue = discussionFiltersMock[0].value;
store.state.discussions = discussions;
vm = mountComponentWithStore(Component, {
@@ -25,7 +27,7 @@ describe('DiscussionFilter component', () => {
store,
props: {
filters: discussionFiltersMock,
- defaultValue,
+ selectedValue,
},
});
});
@@ -35,11 +37,15 @@ describe('DiscussionFilter component', () => {
});
it('renders the all filters', () => {
- expect(vm.$el.querySelectorAll('.dropdown-menu li').length).toEqual(discussionFiltersMock.length);
+ expect(vm.$el.querySelectorAll('.dropdown-menu li').length).toEqual(
+ discussionFiltersMock.length,
+ );
});
it('renders the default selected item', () => {
- expect(vm.$el.querySelector('#discussion-filter-dropdown').textContent.trim()).toEqual(discussionFiltersMock[0].title);
+ expect(vm.$el.querySelector('#discussion-filter-dropdown').textContent.trim()).toEqual(
+ discussionFiltersMock[0].title,
+ );
});
it('updates to the selected item', () => {
@@ -57,4 +63,24 @@ describe('DiscussionFilter component', () => {
expect(vm.filterDiscussion).not.toHaveBeenCalled();
});
+
+ it('disables commenting when "Show history only" filter is applied', () => {
+ const filterItem = vm.$el.querySelector('.dropdown-menu li:last-child button');
+ filterItem.click();
+
+ expect(vm.$store.state.commentsDisabled).toBe(true);
+ });
+
+ it('enables commenting when "Show history only" filter is not applied', () => {
+ const filterItem = vm.$el.querySelector('.dropdown-menu li:first-child button');
+ filterItem.click();
+
+ expect(vm.$store.state.commentsDisabled).toBe(false);
+ });
+
+ it('renders a dropdown divider for the default filter', () => {
+ const defaultFilter = vm.$el.querySelector('.dropdown-menu li:first-child');
+
+ expect(defaultFilter.lastChild.classList).toContain('dropdown-divider');
+ });
});
diff --git a/spec/javascripts/notes/components/note_actions_spec.js b/spec/javascripts/notes/components/note_actions_spec.js
index d7298cb3483..f6c854e6def 100644
--- a/spec/javascripts/notes/components/note_actions_spec.js
+++ b/spec/javascripts/notes/components/note_actions_spec.js
@@ -55,7 +55,7 @@ describe('issue_note_actions component', () => {
expect(vm.$el.querySelector('.js-note-edit')).toBeDefined();
});
- it('should be possible to report as abuse', () => {
+ it('should be possible to report abuse to GitLab', () => {
expect(vm.$el.querySelector(`a[href="${props.reportAbusePath}"]`)).toBeDefined();
});
diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js
index 06b30375306..0081f42c330 100644
--- a/spec/javascripts/notes/components/note_app_spec.js
+++ b/spec/javascripts/notes/components/note_app_spec.js
@@ -97,7 +97,8 @@ describe('note_app', () => {
});
it('should render list of notes', done => {
- const note = mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET[
+ const note =
+ mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET[
'/gitlab-org/gitlab-ce/issues/26/discussions.json'
][0].notes[0];
@@ -120,6 +121,13 @@ describe('note_app', () => {
).toEqual('Write a comment or drag your files here…');
});
+ it('should not render form when commenting is disabled', () => {
+ store.state.commentsDisabled = true;
+ vm = mountComponent();
+
+ expect(vm.$el.querySelector('.js-main-target-form')).toEqual(null);
+ });
+
it('should render form comment button as disabled', () => {
expect(vm.$el.querySelector('.js-note-new-discussion').getAttribute('disabled')).toEqual(
'disabled',
diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js
index b447e79b0df..81cb3e1f74d 100644
--- a/spec/javascripts/notes/components/noteable_discussion_spec.js
+++ b/spec/javascripts/notes/components/noteable_discussion_spec.js
@@ -3,6 +3,7 @@ import createStore from '~/notes/stores';
import noteableDiscussion from '~/notes/components/noteable_discussion.vue';
import '~/behaviors/markdown/render_gfm';
import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data';
+import mockDiffFile from '../../diffs/mock_data/diff_file';
const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json';
@@ -33,9 +34,20 @@ describe('noteable_discussion component', () => {
expect(vm.$el.querySelector('.user-avatar-link')).not.toBeNull();
});
+ it('should not render discussion header for non diff discussions', () => {
+ expect(vm.$el.querySelector('.discussion-header')).toBeNull();
+ });
+
it('should render discussion header', () => {
- expect(vm.$el.querySelector('.discussion-header')).not.toBeNull();
- expect(vm.$el.querySelector('.notes').children.length).toEqual(discussionMock.notes.length);
+ const discussion = { ...discussionMock };
+ discussion.diff_file = mockDiffFile;
+ discussion.diff_discussion = true;
+ const diffDiscussionVm = new Component({
+ store,
+ propsData: { discussion },
+ }).$mount();
+
+ expect(diffDiscussionVm.$el.querySelector('.discussion-header')).not.toBeNull();
});
describe('actions', () => {
diff --git a/spec/javascripts/notes/components/toggle_replies_widget_spec.js b/spec/javascripts/notes/components/toggle_replies_widget_spec.js
new file mode 100644
index 00000000000..2ead8cc6e6a
--- /dev/null
+++ b/spec/javascripts/notes/components/toggle_replies_widget_spec.js
@@ -0,0 +1,78 @@
+import Vue from 'vue';
+import toggleRepliesWidget from '~/notes/components/toggle_replies_widget.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { note } from '../mock_data';
+
+const deepCloneObject = obj => JSON.parse(JSON.stringify(obj));
+
+describe('toggle replies widget for notes', () => {
+ let vm;
+ let ToggleRepliesWidget;
+ const noteFromOtherUser = deepCloneObject(note);
+ noteFromOtherUser.author.username = 'fatihacet';
+
+ const noteFromAnotherUser = deepCloneObject(note);
+ noteFromAnotherUser.author.username = 'mgreiling';
+ noteFromAnotherUser.author.name = 'Mike Greiling';
+
+ const replies = [note, note, note, noteFromOtherUser, noteFromAnotherUser];
+
+ beforeEach(() => {
+ ToggleRepliesWidget = Vue.extend(toggleRepliesWidget);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('collapsed state', () => {
+ beforeEach(() => {
+ vm = mountComponent(ToggleRepliesWidget, {
+ replies,
+ collapsed: true,
+ });
+ });
+
+ it('should render the collapsed', () => {
+ const vmTextContent = vm.$el.textContent.replace(/\s\s+/g, ' ');
+
+ expect(vm.$el.classList.contains('collapsed')).toEqual(true);
+ expect(vm.$el.querySelectorAll('.user-avatar-link').length).toEqual(3);
+ expect(vm.$el.querySelector('time')).not.toBeNull();
+ expect(vmTextContent).toContain('5 replies');
+ expect(vmTextContent).toContain(`Last reply by ${noteFromAnotherUser.author.name}`);
+ });
+
+ it('should emit toggle event when the replies text clicked', () => {
+ const spy = spyOn(vm, '$emit');
+
+ vm.$el.querySelector('.js-replies-text').click();
+
+ expect(spy).toHaveBeenCalledWith('toggle');
+ });
+ });
+
+ describe('expanded state', () => {
+ beforeEach(() => {
+ vm = mountComponent(ToggleRepliesWidget, {
+ replies,
+ collapsed: false,
+ });
+ });
+
+ it('should render expanded state', () => {
+ const vmTextContent = vm.$el.textContent.replace(/\s\s+/g, ' ');
+
+ expect(vm.$el.querySelector('.collapse-replies-btn')).not.toBeNull();
+ expect(vmTextContent).toContain('Collapse replies');
+ });
+
+ it('should emit toggle event when the collapse replies text called', () => {
+ const spy = spyOn(vm, '$emit');
+
+ vm.$el.querySelector('.js-collapse-replies').click();
+
+ expect(spy).toHaveBeenCalledWith('toggle');
+ });
+ });
+});
diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js
index f4643fd55ed..fcdd834e4a0 100644
--- a/spec/javascripts/notes/stores/actions_spec.js
+++ b/spec/javascripts/notes/stores/actions_spec.js
@@ -396,6 +396,9 @@ describe('Actions Notes Store', () => {
{
type: 'updateMergeRequestWidget',
},
+ {
+ type: 'startTaskList',
+ },
],
done,
);
@@ -509,4 +512,17 @@ describe('Actions Notes Store', () => {
expect(mrWidgetEventHub.$emit).toHaveBeenCalledWith('mr.discussion.updated');
});
});
+
+ describe('setCommentsDisabled', () => {
+ it('should set comments disabled state', done => {
+ testAction(
+ actions.setCommentsDisabled,
+ true,
+ null,
+ [{ type: 'DISABLE_COMMENTS', payload: true }],
+ [],
+ done,
+ );
+ });
+ });
});
diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js
index 9d652ba9f1e..461de5a3106 100644
--- a/spec/javascripts/notes/stores/mutation_spec.js
+++ b/spec/javascripts/notes/stores/mutation_spec.js
@@ -78,7 +78,7 @@ describe('Notes Store mutations', () => {
});
describe('COLLAPSE_DISCUSSION', () => {
- it('should collpase an expanded discussion', () => {
+ it('should collapse an expanded discussion', () => {
const discussion = Object.assign({}, discussionMock, { expanded: true });
const state = {
@@ -427,4 +427,14 @@ describe('Notes Store mutations', () => {
expect(state.discussions[0].expanded).toBe(true);
});
});
+
+ describe('DISABLE_COMMENTS', () => {
+ it('should set comments disabled state', () => {
+ const state = {};
+
+ mutations.DISABLE_COMMENTS(state, true);
+
+ expect(state.commentsDisabled).toEqual(true);
+ });
+ });
});
diff --git a/spec/javascripts/pipelines/empty_state_spec.js b/spec/javascripts/pipelines/empty_state_spec.js
index e21dca45fa1..f12950b8fce 100644
--- a/spec/javascripts/pipelines/empty_state_spec.js
+++ b/spec/javascripts/pipelines/empty_state_spec.js
@@ -24,7 +24,7 @@ describe('Pipelines Empty State', () => {
expect(component.$el.querySelector('.svg-content svg')).toBeDefined();
});
- it('should render emtpy state information', () => {
+ it('should render empty state information', () => {
expect(component.$el.querySelector('h4').textContent).toContain('Build with confidence');
expect(
diff --git a/spec/javascripts/pipelines/graph/action_component_spec.js b/spec/javascripts/pipelines/graph/action_component_spec.js
index 027066e1d4d..3d2232ff239 100644
--- a/spec/javascripts/pipelines/graph/action_component_spec.js
+++ b/spec/javascripts/pipelines/graph/action_component_spec.js
@@ -50,7 +50,7 @@ describe('pipeline graph action component', () => {
});
describe('on click', () => {
- it('emits `pipelineActionRequestComplete` after a successfull request', done => {
+ it('emits `pipelineActionRequestComplete` after a successful request', done => {
spyOn(component, '$emit');
component.$el.click();
diff --git a/spec/javascripts/pipelines/graph/graph_component_spec.js b/spec/javascripts/pipelines/graph/graph_component_spec.js
index b6fa4272c8b..96a2d5f62fa 100644
--- a/spec/javascripts/pipelines/graph/graph_component_spec.js
+++ b/spec/javascripts/pipelines/graph/graph_component_spec.js
@@ -40,7 +40,9 @@ describe('graph component', () => {
).toEqual(true);
expect(
- component.$el.querySelector('.stage-column:nth-child(2) .build:nth-child(1)').classList.contains('left-connector'),
+ component.$el
+ .querySelector('.stage-column:nth-child(2) .build:nth-child(1)')
+ .classList.contains('left-connector'),
).toEqual(true);
expect(component.$el.querySelector('loading-icon')).toBe(null);
@@ -56,7 +58,9 @@ describe('graph component', () => {
pipeline: graphJSON,
});
- expect(component.$el.querySelector('.stage-column:nth-child(2) .stage-name').textContent.trim()).toEqual('Deploy &lt;img src=x onerror=alert(document.domain)&gt;');
+ expect(
+ component.$el.querySelector('.stage-column:nth-child(2) .stage-name').textContent.trim(),
+ ).toEqual('Deploy &lt;img src=x onerror=alert(document.domain)&gt;');
});
});
});
diff --git a/spec/javascripts/pipelines/graph/job_item_spec.js b/spec/javascripts/pipelines/graph/job_item_spec.js
index 7cbcdc791e7..88e1789184d 100644
--- a/spec/javascripts/pipelines/graph/job_item_spec.js
+++ b/spec/javascripts/pipelines/graph/job_item_spec.js
@@ -6,6 +6,7 @@ describe('pipeline graph job item', () => {
const JobComponent = Vue.extend(JobItem);
let component;
+ const delayedJobFixture = getJSONFixture('jobs/delayed.json');
const mockJob = {
id: 4256,
name: 'test',
@@ -139,14 +140,12 @@ describe('pipeline graph job item', () => {
});
describe('tooltip placement', () => {
- const tooltipBoundary = 'a[data-boundary="viewport"]';
-
it('does not set tooltip boundary by default', () => {
component = mountComponent(JobComponent, {
job: mockJob,
});
- expect(component.$el.querySelector(tooltipBoundary)).toBeNull();
+ expect(component.tooltipBoundary).toBeNull();
});
it('sets tooltip boundary to viewport for small dropdowns', () => {
@@ -155,7 +154,7 @@ describe('pipeline graph job item', () => {
dropdownLength: 1,
});
- expect(component.$el.querySelector(tooltipBoundary)).not.toBeNull();
+ expect(component.tooltipBoundary).toEqual('viewport');
});
it('does not set tooltip boundary for large lists', () => {
@@ -164,7 +163,33 @@ describe('pipeline graph job item', () => {
dropdownLength: 7,
});
- expect(component.$el.querySelector(tooltipBoundary)).toBeNull();
+ expect(component.tooltipBoundary).toBeNull();
+ });
+ });
+
+ describe('for delayed job', () => {
+ beforeEach(() => {
+ const fifteenMinutesInMilliseconds = 900000;
+ spyOn(Date, 'now').and.callFake(
+ () => new Date(delayedJobFixture.scheduled_at).getTime() - fifteenMinutesInMilliseconds,
+ );
+ });
+
+ it('displays remaining time in tooltip', done => {
+ component = mountComponent(JobComponent, {
+ job: delayedJobFixture,
+ });
+
+ Vue.nextTick()
+ .then(() => {
+ expect(
+ component.$el
+ .querySelector('.js-pipeline-graph-job-link')
+ .getAttribute('data-original-title'),
+ ).toEqual('delayed job - delayed manual action (00:15:00)');
+ })
+ .then(done)
+ .catch(done.fail);
});
});
});
diff --git a/spec/javascripts/pipelines/header_component_spec.js b/spec/javascripts/pipelines/header_component_spec.js
index 473a062fc40..556a0976b29 100644
--- a/spec/javascripts/pipelines/header_component_spec.js
+++ b/spec/javascripts/pipelines/header_component_spec.js
@@ -51,7 +51,7 @@ describe('Pipeline details header', () => {
.querySelector('.header-main-content')
.textContent.replace(/\s+/g, ' ')
.trim(),
- ).toEqual('failed Pipeline #123 triggered 3 weeks ago by Foo');
+ ).toContain('failed Pipeline #123 triggered 3 weeks ago by Foo');
});
describe('action buttons', () => {
diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js
index c9011b403b7..d6c44f4c976 100644
--- a/spec/javascripts/pipelines/pipeline_url_spec.js
+++ b/spec/javascripts/pipelines/pipeline_url_spec.js
@@ -63,12 +63,15 @@ describe('Pipeline Url Component', () => {
}).$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(image.getAttribute('data-original-title')).toEqual(mockData.pipeline.user.name);
+ expect(tooltip.textContent.trim()).toEqual(mockData.pipeline.user.name);
expect(image.getAttribute('src')).toEqual(`${mockData.pipeline.user.avatar_url}?width=20`);
});
diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js
index 37908153e0e..97ded16db69 100644
--- a/spec/javascripts/pipelines/pipelines_spec.js
+++ b/spec/javascripts/pipelines/pipelines_spec.js
@@ -372,7 +372,7 @@ describe('Pipelines', () => {
});
});
- describe('successfull request', () => {
+ describe('successful request', () => {
describe('with pipelines', () => {
beforeEach(() => {
mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
@@ -667,7 +667,7 @@ describe('Pipelines', () => {
});
});
- it('returns false when state is emtpy state', done => {
+ it('returns false when state is empty state', done => {
vm.isLoading = false;
vm.hasMadeRequest = true;
vm.hasGitlabCi = false;
diff --git a/spec/javascripts/pipelines/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js
index 506d01f5ec1..4c575536f0e 100644
--- a/spec/javascripts/pipelines/pipelines_table_row_spec.js
+++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js
@@ -86,8 +86,8 @@ describe('Pipelines Table Row', () => {
expect(
component.$el
- .querySelector('.table-section:nth-child(2) img')
- .getAttribute('data-original-title'),
+ .querySelector('.table-section:nth-child(2) .js-user-avatar-image-toolip')
+ .textContent.trim(),
).toEqual(pipeline.user.name);
});
});
@@ -112,8 +112,8 @@ describe('Pipelines Table Row', () => {
const commitAuthorLink = commitAuthorElement.getAttribute('href');
const commitAuthorName = commitAuthorElement
- .querySelector('img.avatar')
- .getAttribute('data-original-title');
+ .querySelector('.js-user-avatar-image-toolip')
+ .textContent.trim();
return { commitAuthorElement, commitAuthorLink, commitAuthorName };
};
diff --git a/spec/javascripts/pipelines/stage_spec.js b/spec/javascripts/pipelines/stage_spec.js
index a3caaeb44dc..3c8b8032de8 100644
--- a/spec/javascripts/pipelines/stage_spec.js
+++ b/spec/javascripts/pipelines/stage_spec.js
@@ -40,7 +40,7 @@ describe('Pipelines stage component', () => {
expect(component.$el.querySelector('button').getAttribute('data-toggle')).toEqual('dropdown');
});
- describe('with successfull request', () => {
+ describe('with successful request', () => {
beforeEach(() => {
mock.onGet('path.json').reply(200, stageReply);
});
diff --git a/spec/javascripts/reports/components/grouped_test_reports_app_spec.js b/spec/javascripts/reports/components/grouped_test_reports_app_spec.js
index f58515daa4f..69767d9cf1c 100644
--- a/spec/javascripts/reports/components/grouped_test_reports_app_spec.js
+++ b/spec/javascripts/reports/components/grouped_test_reports_app_spec.js
@@ -151,11 +151,11 @@ describe('Grouped Test Reports App', () => {
it('renders resolved failures', done => {
setTimeout(() => {
- expect(vm.$el.querySelector('.js-mr-code-resolved-issues').textContent).toContain(
+ expect(vm.$el.querySelector('.report-block-container').textContent).toContain(
resolvedFailures.suites[0].resolved_failures[0].name,
);
- expect(vm.$el.querySelector('.js-mr-code-resolved-issues').textContent).toContain(
+ expect(vm.$el.querySelector('.report-block-container').textContent).toContain(
resolvedFailures.suites[0].resolved_failures[1].name,
);
done();
diff --git a/spec/javascripts/reports/components/report_section_spec.js b/spec/javascripts/reports/components/report_section_spec.js
index eb7307605d7..b02af8baaec 100644
--- a/spec/javascripts/reports/components/report_section_spec.js
+++ b/spec/javascripts/reports/components/report_section_spec.js
@@ -120,7 +120,7 @@ describe('Report section', () => {
'Code quality improved on 1 point and degraded on 1 point',
);
- expect(vm.$el.querySelectorAll('.js-mr-code-resolved-issues li').length).toEqual(
+ expect(vm.$el.querySelectorAll('.report-block-container li').length).toEqual(
resolvedIssues.length,
);
});
diff --git a/spec/javascripts/sidebar/assignees_spec.js b/spec/javascripts/sidebar/assignees_spec.js
index e7f8f4f9936..eced4925489 100644
--- a/spec/javascripts/sidebar/assignees_spec.js
+++ b/spec/javascripts/sidebar/assignees_spec.js
@@ -78,9 +78,7 @@ describe('Assignee component', () => {
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000',
- users: [
- UsersMock.user,
- ],
+ users: [UsersMock.user],
editable: false,
},
}).$mount();
@@ -90,7 +88,10 @@ describe('Assignee component', () => {
expect(collapsed.childElementCount).toEqual(1);
expect(assignee.querySelector('.avatar').getAttribute('src')).toEqual(UsersMock.user.avatar);
- expect(assignee.querySelector('.avatar').getAttribute('alt')).toEqual(`${UsersMock.user.name}'s avatar`);
+ expect(assignee.querySelector('.avatar').getAttribute('alt')).toEqual(
+ `${UsersMock.user.name}'s avatar`,
+ );
+
expect(assignee.querySelector('.author').innerText.trim()).toEqual(UsersMock.user.name);
});
@@ -98,34 +99,38 @@ describe('Assignee component', () => {
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000/',
- users: [
- UsersMock.user,
- ],
+ users: [UsersMock.user],
editable: true,
},
}).$mount();
expect(component.$el.querySelector('.author-link')).not.toBeNull();
// The image
- expect(component.$el.querySelector('.author-link img').getAttribute('src')).toEqual(UsersMock.user.avatar);
+ expect(component.$el.querySelector('.author-link img').getAttribute('src')).toEqual(
+ UsersMock.user.avatar,
+ );
// Author name
- expect(component.$el.querySelector('.author-link .author').innerText.trim()).toEqual(UsersMock.user.name);
+ expect(component.$el.querySelector('.author-link .author').innerText.trim()).toEqual(
+ UsersMock.user.name,
+ );
// Username
- expect(component.$el.querySelector('.author-link .username').innerText.trim()).toEqual(`@${UsersMock.user.username}`);
+ expect(component.$el.querySelector('.author-link .username').innerText.trim()).toEqual(
+ `@${UsersMock.user.username}`,
+ );
});
it('has the root url present in the assigneeUrl method', () => {
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000/',
- users: [
- UsersMock.user,
- ],
+ users: [UsersMock.user],
editable: true,
},
}).$mount();
- expect(component.assigneeUrl(UsersMock.user).indexOf('http://localhost:3000/')).not.toEqual(-1);
+ expect(component.assigneeUrl(UsersMock.user).indexOf('http://localhost:3000/')).not.toEqual(
+ -1,
+ );
});
});
@@ -147,13 +152,19 @@ describe('Assignee component', () => {
const first = collapsed.children[0];
expect(first.querySelector('.avatar').getAttribute('src')).toEqual(users[0].avatar);
- expect(first.querySelector('.avatar').getAttribute('alt')).toEqual(`${users[0].name}'s avatar`);
+ expect(first.querySelector('.avatar').getAttribute('alt')).toEqual(
+ `${users[0].name}'s avatar`,
+ );
+
expect(first.querySelector('.author').innerText.trim()).toEqual(users[0].name);
const second = collapsed.children[1];
expect(second.querySelector('.avatar').getAttribute('src')).toEqual(users[1].avatar);
- expect(second.querySelector('.avatar').getAttribute('alt')).toEqual(`${users[1].name}'s avatar`);
+ expect(second.querySelector('.avatar').getAttribute('alt')).toEqual(
+ `${users[1].name}'s avatar`,
+ );
+
expect(second.querySelector('.author').innerText.trim()).toEqual(users[1].name);
});
@@ -174,7 +185,10 @@ describe('Assignee component', () => {
const first = collapsed.children[0];
expect(first.querySelector('.avatar').getAttribute('src')).toEqual(users[0].avatar);
- expect(first.querySelector('.avatar').getAttribute('alt')).toEqual(`${users[0].name}'s avatar`);
+ expect(first.querySelector('.avatar').getAttribute('alt')).toEqual(
+ `${users[0].name}'s avatar`,
+ );
+
expect(first.querySelector('.author').innerText.trim()).toEqual(users[0].name);
const second = collapsed.children[1];
@@ -196,7 +210,7 @@ describe('Assignee component', () => {
expect(component.$el.querySelector('.user-list-more')).toBe(null);
});
- it('Shows the "show-less" assignees label', (done) => {
+ it('Shows the "show-less" assignees label', done => {
const users = UsersMockHelper.createNumberRandomUsers(6);
component = new AssigneeComponent({
propsData: {
@@ -206,21 +220,26 @@ describe('Assignee component', () => {
},
}).$mount();
- expect(component.$el.querySelectorAll('.user-item').length).toEqual(component.defaultRenderCount);
+ expect(component.$el.querySelectorAll('.user-item').length).toEqual(
+ component.defaultRenderCount,
+ );
+
expect(component.$el.querySelector('.user-list-more')).not.toBe(null);
const usersLabelExpectation = users.length - component.defaultRenderCount;
- expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim())
- .not.toBe(`+${usersLabelExpectation} more`);
+ expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).not.toBe(
+ `+${usersLabelExpectation} more`,
+ );
component.toggleShowLess();
Vue.nextTick(() => {
- expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim())
- .toBe('- show less');
+ expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe(
+ '- show less',
+ );
done();
});
});
- it('Shows the "show-less" when "n+ more " label is clicked', (done) => {
+ it('Shows the "show-less" when "n+ more " label is clicked', done => {
const users = UsersMockHelper.createNumberRandomUsers(6);
component = new AssigneeComponent({
propsData: {
@@ -232,8 +251,9 @@ describe('Assignee component', () => {
component.$el.querySelector('.user-list-more .btn-link').click();
Vue.nextTick(() => {
- expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim())
- .toBe('- show less');
+ expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe(
+ '- show less',
+ );
done();
});
});
@@ -264,16 +284,18 @@ describe('Assignee component', () => {
});
it('shows "+1 more" label', () => {
- expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim())
- .toBe('+ 1 more');
+ expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe(
+ '+ 1 more',
+ );
});
- it('shows "show less" label', (done) => {
+ it('shows "show less" label', done => {
component.toggleShowLess();
Vue.nextTick(() => {
- expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim())
- .toBe('- show less');
+ expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe(
+ '- show less',
+ );
done();
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/deployment_spec.js b/spec/javascripts/vue_mr_widget/components/deployment_spec.js
index d26ffa4b4a6..ebbcaeb6f30 100644
--- a/spec/javascripts/vue_mr_widget/components/deployment_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/deployment_spec.js
@@ -174,38 +174,14 @@ describe('Deployment component', () => {
});
});
- describe('with `features.ciEnvironmentsStatusChanges` enabled', () => {
+ describe('without changes', () => {
beforeEach(() => {
- window.gon = window.gon || {};
- window.gon.features = window.gon.features || {};
- window.gon.features.ciEnvironmentsStatusChanges = true;
- vm = mountComponent(Component, { deployment: { ...deploymentMockData } });
- });
-
- afterEach(() => {
- window.gon.features = {};
- });
-
- it('renders dropdown with changes', () => {
- expect(vm.$el.querySelector('.js-mr-wigdet-deployment-dropdown')).not.toBeNull();
- expect(vm.$el.querySelector('.js-deploy-url-feature-flag')).toBeNull();
- });
- });
-
- describe('with `features.ciEnvironmentsStatusChanges` disabled', () => {
- beforeEach(() => {
- window.gon = window.gon || {};
- window.gon.features = window.gon.features || {};
- window.gon.features.ciEnvironmentsStatusChanges = false;
+ delete deploymentMockData.changes;
vm = mountComponent(Component, { deployment: { ...deploymentMockData } });
});
- afterEach(() => {
- delete window.gon.features.ciEnvironmentsStatusChanges;
- });
-
- it('renders the old link to the review app', () => {
+ it('renders the link to the review app without dropdown', () => {
expect(vm.$el.querySelector('.js-mr-wigdet-deployment-dropdown')).toBeNull();
expect(vm.$el.querySelector('.js-deploy-url-feature-flag')).not.toBeNull();
});
@@ -222,6 +198,10 @@ describe('Deployment component', () => {
it('renders information about running deployment', () => {
expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain('Deploying to');
});
+
+ it('renders disabled stop button', () => {
+ expect(vm.$el.querySelector('.js-stop-env').getAttribute('disabled')).toBe('disabled');
+ });
});
describe('success', () => {
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 ea8007d2029..d905bbe4040 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
@@ -22,6 +22,7 @@ describe('MRWidgetPipeline', () => {
pipeline: mockData.pipeline,
ciStatus: 'success',
hasCi: true,
+ troubleshootingDocsPath: 'help',
});
expect(vm.hasPipeline).toEqual(true);
@@ -30,6 +31,7 @@ describe('MRWidgetPipeline', () => {
it('should return false when there is no pipeline', () => {
vm = mountComponent(Component, {
pipeline: {},
+ troubleshootingDocsPath: 'help',
});
expect(vm.hasPipeline).toEqual(false);
@@ -42,6 +44,7 @@ describe('MRWidgetPipeline', () => {
pipeline: mockData.pipeline,
hasCi: true,
ciStatus: 'success',
+ troubleshootingDocsPath: 'help',
});
expect(vm.hasCIError).toEqual(false);
@@ -52,6 +55,7 @@ describe('MRWidgetPipeline', () => {
pipeline: mockData.pipeline,
hasCi: true,
ciStatus: null,
+ troubleshootingDocsPath: 'help',
});
expect(vm.hasCIError).toEqual(true);
@@ -65,11 +69,12 @@ describe('MRWidgetPipeline', () => {
pipeline: mockData.pipeline,
hasCi: true,
ciStatus: null,
+ troubleshootingDocsPath: 'help',
});
- expect(
- vm.$el.querySelector('.media-body').textContent.trim(),
- ).toEqual('Could not connect to the CI server. Please check your settings and try again');
+ 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', () => {
@@ -78,38 +83,41 @@ describe('MRWidgetPipeline', () => {
pipeline: mockData.pipeline,
hasCi: true,
ciStatus: 'success',
+ troubleshootingDocsPath: 'help',
});
});
it('should render pipeline ID', () => {
- expect(
- vm.$el.querySelector('.pipeline-id').textContent.trim(),
- ).toEqual(`#${mockData.pipeline.id}`);
+ expect(vm.$el.querySelector('.pipeline-id').textContent.trim()).toEqual(
+ `#${mockData.pipeline.id}`,
+ );
});
it('should render pipeline status and commit id', () => {
- expect(
- vm.$el.querySelector('.media-body').textContent.trim(),
- ).toContain(mockData.pipeline.details.status.label);
+ expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain(
+ mockData.pipeline.details.status.label,
+ );
- expect(
- vm.$el.querySelector('.js-commit-link').textContent.trim(),
- ).toEqual(mockData.pipeline.commit.short_id);
+ expect(vm.$el.querySelector('.js-commit-link').textContent.trim()).toEqual(
+ mockData.pipeline.commit.short_id,
+ );
- expect(
- vm.$el.querySelector('.js-commit-link').getAttribute('href'),
- ).toEqual(mockData.pipeline.commit.commit_path);
+ expect(vm.$el.querySelector('.js-commit-link').getAttribute('href')).toEqual(
+ mockData.pipeline.commit.commit_path,
+ );
});
it('should render pipeline graph', () => {
expect(vm.$el.querySelector('.mr-widget-pipeline-graph')).toBeDefined();
- expect(vm.$el.querySelectorAll('.stage-container').length).toEqual(mockData.pipeline.details.stages.length);
+ expect(vm.$el.querySelectorAll('.stage-container').length).toEqual(
+ mockData.pipeline.details.stages.length,
+ );
});
it('should render coverage information', () => {
- expect(
- vm.$el.querySelector('.media-body').textContent,
- ).toContain(`Coverage ${mockData.pipeline.coverage}`);
+ expect(vm.$el.querySelector('.media-body').textContent).toContain(
+ `Coverage ${mockData.pipeline.coverage}`,
+ );
});
});
@@ -122,34 +130,35 @@ describe('MRWidgetPipeline', () => {
pipeline: mockCopy.pipeline,
hasCi: true,
ciStatus: 'success',
+ troubleshootingDocsPath: 'help',
});
});
it('should render pipeline ID', () => {
- expect(
- vm.$el.querySelector('.pipeline-id').textContent.trim(),
- ).toEqual(`#${mockData.pipeline.id}`);
+ expect(vm.$el.querySelector('.pipeline-id').textContent.trim()).toEqual(
+ `#${mockData.pipeline.id}`,
+ );
});
it('should render pipeline status', () => {
- expect(
- vm.$el.querySelector('.media-body').textContent.trim(),
- ).toContain(mockData.pipeline.details.status.label);
+ expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain(
+ mockData.pipeline.details.status.label,
+ );
- expect(
- vm.$el.querySelector('.js-commit-link'),
- ).toBeNull();
+ expect(vm.$el.querySelector('.js-commit-link')).toBeNull();
});
it('should render pipeline graph', () => {
expect(vm.$el.querySelector('.mr-widget-pipeline-graph')).toBeDefined();
- expect(vm.$el.querySelectorAll('.stage-container').length).toEqual(mockData.pipeline.details.stages.length);
+ expect(vm.$el.querySelectorAll('.stage-container').length).toEqual(
+ mockData.pipeline.details.stages.length,
+ );
});
it('should render coverage information', () => {
- expect(
- vm.$el.querySelector('.media-body').textContent,
- ).toContain(`Coverage ${mockData.pipeline.coverage}`);
+ expect(vm.$el.querySelector('.media-body').textContent).toContain(
+ `Coverage ${mockData.pipeline.coverage}`,
+ );
});
});
@@ -162,11 +171,10 @@ describe('MRWidgetPipeline', () => {
pipeline: mockCopy.pipeline,
hasCi: true,
ciStatus: 'success',
+ troubleshootingDocsPath: 'help',
});
- expect(
- vm.$el.querySelector('.media-body').textContent,
- ).not.toContain('Coverage');
+ expect(vm.$el.querySelector('.media-body').textContent).not.toContain('Coverage');
});
});
@@ -179,6 +187,7 @@ describe('MRWidgetPipeline', () => {
pipeline: mockCopy.pipeline,
hasCi: true,
ciStatus: 'success',
+ troubleshootingDocsPath: 'help',
});
expect(vm.$el.querySelector('.js-mini-pipeline-graph')).toEqual(null);
diff --git a/spec/javascripts/vue_mr_widget/components/review_app_link_spec.js b/spec/javascripts/vue_mr_widget/components/review_app_link_spec.js
new file mode 100644
index 00000000000..68a65bd21c6
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/review_app_link_spec.js
@@ -0,0 +1,38 @@
+import Vue from 'vue';
+import component from '~/vue_merge_request_widget/components/review_app_link.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('review app link', () => {
+ const Component = Vue.extend(component);
+ const props = {
+ link: '/review',
+ cssClass: 'js-link',
+ };
+ let vm;
+ let el;
+
+ beforeEach(() => {
+ vm = mountComponent(Component, props);
+ el = vm.$el;
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders provided link as href attribute', () => {
+ expect(el.getAttribute('href')).toEqual(props.link);
+ });
+
+ it('renders provided cssClass as class attribute', () => {
+ expect(el.getAttribute('class')).toEqual(props.cssClass);
+ });
+
+ it('renders View app text', () => {
+ expect(el.textContent.trim()).toEqual('View app');
+ });
+
+ it('renders svg icon', () => {
+ expect(el.querySelector('svg')).not.toBeNull();
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
index d68342635ef..da5cb752c6f 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
@@ -69,7 +69,7 @@ describe('MRWidgetMerged', () => {
expect(vm.shouldShowRemoveSourceBranch).toEqual(true);
});
- it('returns false wehn sourceBranchRemoved is true', () => {
+ it('returns false when sourceBranchRemoved is true', () => {
vm.mr.sourceBranchRemoved = true;
expect(vm.shouldShowRemoveSourceBranch).toEqual(false);
diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js
index 7fd1a2350f7..17554c4fe42 100644
--- a/spec/javascripts/vue_mr_widget/mock_data.js
+++ b/spec/javascripts/vue_mr_widget/mock_data.js
@@ -218,5 +218,7 @@ export default {
diverged_commits_count: 0,
only_allow_merge_if_pipeline_succeeds: false,
commit_change_content_path: '/root/acets-app/merge_requests/22/commit_change_content',
- merge_commit_path: 'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775',
+ merge_commit_path:
+ 'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775',
+ troubleshooting_docs_path: 'help',
};
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 27b6c91e154..f72bf627c10 100644
--- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
@@ -454,14 +454,10 @@ describe('mrWidgetOptions', () => {
deployed_at: '2017-03-22T22:44:42.258Z',
deployed_at_formatted: 'Mar 22, 2017 10:44pm',
changes,
- status: 'success'
+ status: 'success',
};
beforeEach(done => {
- window.gon = window.gon || {};
- window.gon.features = window.gon.features || {};
- window.gon.features.ciEnvironmentsStatusChanges = true;
-
vm.mr.deployments.push(
{
...deploymentMockData,
@@ -607,33 +603,36 @@ describe('mrWidgetOptions', () => {
describe('with post merge deployments', () => {
beforeEach(done => {
- vm.mr.postMergeDeployments = [{
- id: 15,
- name: 'review/diplo',
- url: '/root/acets-review-apps/environments/15',
- stop_url: '/root/acets-review-apps/environments/15/stop',
- metrics_url: '/root/acets-review-apps/environments/15/deployments/1/metrics',
- metrics_monitoring_url: '/root/acets-review-apps/environments/15/metrics',
- external_url: 'http://diplo.',
- external_url_formatted: 'diplo.',
- deployed_at: '2017-03-22T22:44:42.258Z',
- deployed_at_formatted: 'Mar 22, 2017 10:44pm',
- changes: [
- {
- path: 'index.html',
- external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html',
- },
- {
- path: 'imgs/gallery.html',
- external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html',
- },
- {
- path: 'about/',
- external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/',
- },
- ],
- status: 'success'
- }];
+ vm.mr.postMergeDeployments = [
+ {
+ id: 15,
+ name: 'review/diplo',
+ url: '/root/acets-review-apps/environments/15',
+ stop_url: '/root/acets-review-apps/environments/15/stop',
+ metrics_url: '/root/acets-review-apps/environments/15/deployments/1/metrics',
+ metrics_monitoring_url: '/root/acets-review-apps/environments/15/metrics',
+ external_url: 'http://diplo.',
+ external_url_formatted: 'diplo.',
+ deployed_at: '2017-03-22T22:44:42.258Z',
+ deployed_at_formatted: 'Mar 22, 2017 10:44pm',
+ changes: [
+ {
+ path: 'index.html',
+ external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html',
+ },
+ {
+ path: 'imgs/gallery.html',
+ external_url:
+ 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html',
+ },
+ {
+ path: 'about/',
+ external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/',
+ },
+ ],
+ status: 'success',
+ },
+ ];
vm.$nextTick(done);
});
diff --git a/spec/javascripts/vue_shared/components/clipboard_button_spec.js b/spec/javascripts/vue_shared/components/clipboard_button_spec.js
index 2f7ea077b54..fd17349d48f 100644
--- a/spec/javascripts/vue_shared/components/clipboard_button_spec.js
+++ b/spec/javascripts/vue_shared/components/clipboard_button_spec.js
@@ -27,8 +27,6 @@ describe('clipboard button', () => {
it('should have a tooltip with default values', () => {
expect(vm.$el.getAttribute('data-original-title')).toEqual('Copy this value into Clipboard!');
- expect(vm.$el.getAttribute('data-placement')).toEqual('top');
- expect(vm.$el.getAttribute('data-container')).toEqual(null);
});
it('should render provided classname', () => {
diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js
index 97dacec1fce..18fcdf7ede1 100644
--- a/spec/javascripts/vue_shared/components/commit_spec.js
+++ b/spec/javascripts/vue_shared/components/commit_spec.js
@@ -98,8 +98,8 @@ describe('Commit component', () => {
it('Should render the author avatar with title and alt attributes', () => {
expect(
component.$el
- .querySelector('.commit-title .avatar-image-container img')
- .getAttribute('data-original-title'),
+ .querySelector('.commit-title .avatar-image-container .js-user-avatar-image-toolip')
+ .textContent.trim(),
).toContain(props.author.username);
expect(
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 e2c34508b0d..4da8c6196b1 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
@@ -47,7 +47,7 @@ describe('ContentViewer', () => {
});
setTimeout(() => {
- expect(vm.$el.querySelector('.image_file img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL);
+ expect(vm.$el.querySelector('img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL);
done();
});
diff --git a/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js b/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js
index fcd231ec693..67a3a2e08bc 100644
--- a/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js
+++ b/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js
@@ -30,11 +30,11 @@ describe('DiffViewer', () => {
});
setTimeout(() => {
- expect(vm.$el.querySelector('.deleted .image_file img').getAttribute('src')).toBe(
+ expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(
`//raw/DEF/${RED_BOX_IMAGE_URL}`,
);
- expect(vm.$el.querySelector('.added .image_file img').getAttribute('src')).toBe(
+ expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(
`//raw/ABC/${GREEN_BOX_IMAGE_URL}`,
);
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 380effdb669..7f2e246d656 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
@@ -52,13 +52,9 @@ describe('ImageDiffViewer', () => {
});
setTimeout(() => {
- expect(vm.$el.querySelector('.added .image_file img').getAttribute('src')).toBe(
- GREEN_BOX_IMAGE_URL,
- );
+ expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL);
- expect(vm.$el.querySelector('.deleted .image_file img').getAttribute('src')).toBe(
- RED_BOX_IMAGE_URL,
- );
+ expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(RED_BOX_IMAGE_URL);
expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe('2-up');
expect(vm.$el.querySelector('.view-modes-menu li:nth-child(2)').textContent.trim()).toBe(
@@ -81,9 +77,7 @@ describe('ImageDiffViewer', () => {
});
setTimeout(() => {
- expect(vm.$el.querySelector('.added .image_file img').getAttribute('src')).toBe(
- GREEN_BOX_IMAGE_URL,
- );
+ expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL);
done();
});
@@ -97,9 +91,27 @@ describe('ImageDiffViewer', () => {
});
setTimeout(() => {
- expect(vm.$el.querySelector('.deleted .image_file img').getAttribute('src')).toBe(
- RED_BOX_IMAGE_URL,
- );
+ expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(RED_BOX_IMAGE_URL);
+
+ done();
+ });
+ });
+
+ it('renders image diff for renamed', done => {
+ vm = new Vue({
+ components: {
+ imageDiffViewer,
+ },
+ template: `
+ <image-diff-viewer diff-mode="renamed" new-path="${GREEN_BOX_IMAGE_URL}" old-path="">
+ <span slot="image-overlay" class="overlay">test</span>
+ </image-diff-viewer>
+ `,
+ }).$mount();
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL);
+ expect(vm.$el.querySelector('.overlay')).not.toBe(null);
done();
});
diff --git a/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js b/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js
index b71cb36ecf6..b84b5ae67a8 100644
--- a/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js
+++ b/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js
@@ -41,7 +41,7 @@ describe('Filtered search dropdown', () => {
});
});
- describe('when visible number is bigger than the items lenght', () => {
+ describe('when visible number is bigger than the items length', () => {
beforeEach(() => {
vm = mountComponent(Component, {
items: [{ title: 'One' }, { title: 'Two' }, { title: 'Three' }],
diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/javascripts/vue_shared/components/header_ci_component_spec.js
index 3bf497bc00b..7a741bdc067 100644
--- a/spec/javascripts/vue_shared/components/header_ci_component_spec.js
+++ b/spec/javascripts/vue_shared/components/header_ci_component_spec.js
@@ -73,7 +73,7 @@ describe('Header CI Component', () => {
});
it('should render user icon and name', () => {
- expect(vm.$el.querySelector('.js-user-link').textContent.trim()).toEqual(props.user.name);
+ expect(vm.$el.querySelector('.js-user-link').innerText.trim()).toContain(props.user.name);
});
it('should render provided actions', () => {
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
index 3483b7d387d..c507a97d37e 100644
--- 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
@@ -12,7 +12,7 @@ describe('collapsedGroupedDatePicker', () => {
});
describe('toggleCollapse events', () => {
- beforeEach((done) => {
+ beforeEach(done => {
spyOn(vm, 'toggleSidebar');
vm.minDate = new Date('07/17/2016');
Vue.nextTick(done);
@@ -26,7 +26,7 @@ describe('collapsedGroupedDatePicker', () => {
});
describe('minDate and maxDate', () => {
- beforeEach((done) => {
+ beforeEach(done => {
vm.minDate = new Date('07/17/2016');
vm.maxDate = new Date('07/17/2017');
Vue.nextTick(done);
@@ -42,7 +42,7 @@ describe('collapsedGroupedDatePicker', () => {
});
describe('minDate', () => {
- beforeEach((done) => {
+ beforeEach(done => {
vm.minDate = new Date('07/17/2016');
Vue.nextTick(done);
});
@@ -56,7 +56,7 @@ describe('collapsedGroupedDatePicker', () => {
});
describe('maxDate', () => {
- beforeEach((done) => {
+ beforeEach(done => {
vm.maxDate = new Date('07/17/2017');
Vue.nextTick(done);
});
diff --git a/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js b/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js
index 1581f4e3eb1..805ba7b9947 100644
--- a/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js
@@ -41,7 +41,7 @@ describe('sidebarDatePicker', () => {
expect(vm.$el.querySelector('.value-content span').innerText.trim()).toEqual('None');
});
- it('should render date-picker when editing', (done) => {
+ it('should render date-picker when editing', done => {
vm.editing = true;
Vue.nextTick(() => {
expect(vm.$el.querySelector('.pika-label')).toBeDefined();
@@ -50,7 +50,7 @@ describe('sidebarDatePicker', () => {
});
describe('editable', () => {
- beforeEach((done) => {
+ beforeEach(done => {
vm.editable = true;
Vue.nextTick(done);
});
@@ -59,7 +59,7 @@ describe('sidebarDatePicker', () => {
expect(vm.$el.querySelector('.title .btn-blank').innerText.trim()).toEqual('Edit');
});
- it('should enable editing when edit button is clicked', (done) => {
+ it('should enable editing when edit button is clicked', done => {
vm.isLoading = false;
Vue.nextTick(() => {
vm.$el.querySelector('.title .btn-blank').click();
@@ -70,7 +70,7 @@ describe('sidebarDatePicker', () => {
});
});
- it('should render date if selectedDate', (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');
@@ -79,7 +79,7 @@ describe('sidebarDatePicker', () => {
});
describe('selectedDate and editable', () => {
- beforeEach((done) => {
+ beforeEach(done => {
vm.selectedDate = new Date('07/07/2017');
vm.editable = true;
Vue.nextTick(done);
@@ -100,7 +100,7 @@ describe('sidebarDatePicker', () => {
});
describe('showToggleSidebar', () => {
- beforeEach((done) => {
+ beforeEach(done => {
vm.showToggleSidebar = true;
Vue.nextTick(done);
});
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
index 9a691116cf8..804b33422bd 100644
--- 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
@@ -49,7 +49,9 @@ describe('DropdownValueCollapsedComponent', () => {
const vmMoreLabels = createComponent(mockMoreLabels);
- expect(vmMoreLabels.labelsList).toBe('Foo Label, Foo Label, Foo Label, Foo Label, Foo Label, and 2 more');
+ expect(vmMoreLabels.labelsList).toBe(
+ 'Foo Label, Foo Label, Foo Label, Foo Label, Foo Label, and 2 more',
+ );
vmMoreLabels.$destroy();
});
diff --git a/spec/javascripts/vue_shared/components/smart_virtual_list_spec.js b/spec/javascripts/vue_shared/components/smart_virtual_list_spec.js
new file mode 100644
index 00000000000..e723fead65e
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/smart_virtual_list_spec.js
@@ -0,0 +1,83 @@
+import Vue from 'vue';
+import SmartVirtualScrollList from '~/vue_shared/components/smart_virtual_list.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('Toggle Button', () => {
+ let vm;
+
+ const createComponent = ({ length, remain }) => {
+ const smartListProperties = {
+ rtag: 'section',
+ wtag: 'ul',
+ wclass: 'test-class',
+ // Size in pixels does not matter for our tests here
+ size: 35,
+ length,
+ remain,
+ };
+
+ const Component = Vue.extend({
+ components: {
+ SmartVirtualScrollList,
+ },
+ smartListProperties,
+ items: Array(length).fill(1),
+ template: `
+ <smart-virtual-scroll-list v-bind="$options.smartListProperties">
+ <li v-for="(val, key) in $options.items" :key="key">{{ key + 1 }}</li>
+ </smart-virtual-scroll-list>`,
+ });
+
+ return mountComponent(Component);
+ };
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('if the list is shorter than the maximum shown elements', () => {
+ const listLength = 10;
+
+ beforeEach(() => {
+ vm = createComponent({ length: listLength, remain: 20 });
+ });
+
+ it('renders without the vue-virtual-scroll-list component', () => {
+ expect(vm.$el.classList).not.toContain('js-virtual-list');
+ expect(vm.$el.classList).toContain('js-plain-element');
+ });
+
+ it('renders list with provided tags and classes for the wrapper elements', () => {
+ expect(vm.$el.tagName).toEqual('SECTION');
+ expect(vm.$el.firstChild.tagName).toEqual('UL');
+ expect(vm.$el.firstChild.classList).toContain('test-class');
+ });
+
+ it('renders all children list elements', () => {
+ expect(vm.$el.querySelectorAll('li').length).toEqual(listLength);
+ });
+ });
+
+ describe('if the list is longer than the maximum shown elements', () => {
+ const maxItemsShown = 20;
+
+ beforeEach(() => {
+ vm = createComponent({ length: 1000, remain: maxItemsShown });
+ });
+
+ it('uses the vue-virtual-scroll-list component', () => {
+ expect(vm.$el.classList).toContain('js-virtual-list');
+ expect(vm.$el.classList).not.toContain('js-plain-element');
+ });
+
+ it('renders list with provided tags and classes for the wrapper elements', () => {
+ expect(vm.$el.tagName).toEqual('SECTION');
+ expect(vm.$el.firstChild.tagName).toEqual('UL');
+ expect(vm.$el.firstChild.classList).toContain('test-class');
+ });
+
+ it('renders at max twice the maximum shown elements', () => {
+ expect(vm.$el.querySelectorAll('li').length).toBeLessThanOrEqual(2 * maxItemsShown);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
index dc7652c77f7..5c4aa7cf844 100644
--- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
+++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import { placeholderImage } from '~/lazy_loader';
import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent, { mountComponentWithSlots } from 'spec/helpers/vue_mount_component_helper';
const DEFAULT_PROPS = {
size: 99,
@@ -32,18 +32,12 @@ describe('User Avatar Image Component', function() {
});
it('should have <img> as a child element', function() {
- expect(vm.$el.tagName).toBe('IMG');
- expect(vm.$el.getAttribute('src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
- expect(vm.$el.getAttribute('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
- expect(vm.$el.getAttribute('alt')).toBe(DEFAULT_PROPS.imgAlt);
- });
-
- it('should properly compute tooltipContainer', function() {
- expect(vm.tooltipContainer).toBe('body');
- });
+ const imageElement = vm.$el.querySelector('img');
- it('should properly render tooltipContainer', function() {
- expect(vm.$el.getAttribute('data-container')).toBe('body');
+ expect(imageElement).not.toBe(null);
+ expect(imageElement.getAttribute('src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
+ expect(imageElement.getAttribute('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
+ expect(imageElement.getAttribute('alt')).toBe(DEFAULT_PROPS.imgAlt);
});
it('should properly compute avatarSizeClass', function() {
@@ -51,7 +45,7 @@ describe('User Avatar Image Component', function() {
});
it('should properly render img css', function() {
- const { classList } = vm.$el;
+ const { classList } = vm.$el.querySelector('img');
const containsAvatar = classList.contains('avatar');
const containsSizeClass = classList.contains('s99');
const containsCustomClass = classList.contains(DEFAULT_PROPS.cssClasses);
@@ -73,12 +67,41 @@ describe('User Avatar Image Component', function() {
});
it('should add lazy attributes', function() {
- const { classList } = vm.$el;
- const lazyClass = classList.contains('lazy');
+ const imageElement = vm.$el.querySelector('img');
+ const lazyClass = imageElement.classList.contains('lazy');
expect(lazyClass).toBe(true);
- expect(vm.$el.getAttribute('src')).toBe(placeholderImage);
- expect(vm.$el.getAttribute('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
+ expect(imageElement.getAttribute('src')).toBe(placeholderImage);
+ expect(imageElement.getAttribute('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
+ });
+ });
+
+ describe('dynamic tooltip content', () => {
+ const props = DEFAULT_PROPS;
+ const slots = {
+ default: ['Action!'],
+ };
+
+ beforeEach(() => {
+ vm = mountComponentWithSlots(UserAvatarImage, { props, slots }).$mount();
+ });
+
+ it('renders the tooltip slot', () => {
+ expect(vm.$el.querySelector('.js-user-avatar-image-toolip')).not.toBe(null);
+ });
+
+ it('renders the tooltip content', () => {
+ expect(vm.$el.querySelector('.js-user-avatar-image-toolip').textContent).toContain(
+ slots.default[0],
+ );
+ });
+
+ it('does not render tooltip data attributes for on avatar image', () => {
+ const avatarImg = vm.$el.querySelector('img');
+
+ expect(avatarImg.dataset.originalTitle).not.toBeDefined();
+ expect(avatarImg.dataset.placement).not.toBeDefined();
+ expect(avatarImg.dataset.container).not.toBeDefined();
});
});
});
diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
index 50b8d49d4bd..0151ad23ba2 100644
--- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
+++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
@@ -44,7 +44,7 @@ describe('User Avatar Link Component', function() {
expect(this.userAvatarLink.$el.querySelector('img')).not.toBeNull();
});
- it('should return neccessary props as defined', function() {
+ it('should return necessary props as defined', function() {
_.each(this.propsData, (val, key) => {
expect(this.userAvatarLink[key]).toBeDefined();
});
@@ -60,39 +60,43 @@ describe('User Avatar Link Component', function() {
it('should only render image tag in link', function() {
const childElements = this.userAvatarLink.$el.childNodes;
- expect(childElements[0].tagName).toBe('IMG');
+ expect(this.userAvatarLink.$el.querySelector('img')).not.toBe('null');
// Vue will render the hidden component as <!---->
expect(childElements[1].tagName).toBeUndefined();
});
it('should render avatar image tooltip', function() {
- expect(this.userAvatarLink.$el.querySelector('img').dataset.originalTitle).toEqual(
- this.propsData.tooltipText,
- );
+ expect(this.userAvatarLink.shouldShowUsername).toBe(false);
+ expect(this.userAvatarLink.avatarTooltipText).toEqual(this.propsData.tooltipText);
});
});
describe('username', function() {
it('should not render avatar image tooltip', function() {
- expect(this.userAvatarLink.$el.querySelector('img').dataset.originalTitle).toEqual('');
+ expect(
+ this.userAvatarLink.$el.querySelector('.js-user-avatar-image-toolip').innerText.trim(),
+ ).toEqual('');
});
it('should render username prop in <span>', function() {
- expect(this.userAvatarLink.$el.querySelector('span').innerText.trim()).toEqual(
- this.propsData.username,
- );
+ expect(
+ this.userAvatarLink.$el.querySelector('.js-user-avatar-link-username').innerText.trim(),
+ ).toEqual(this.propsData.username);
});
it('should render text tooltip for <span>', function() {
- expect(this.userAvatarLink.$el.querySelector('span').dataset.originalTitle).toEqual(
- this.propsData.tooltipText,
- );
+ expect(
+ this.userAvatarLink.$el.querySelector('.js-user-avatar-link-username').dataset
+ .originalTitle,
+ ).toEqual(this.propsData.tooltipText);
});
it('should render text tooltip placement for <span>', function() {
expect(
- this.userAvatarLink.$el.querySelector('span').getAttribute('tooltip-placement'),
+ this.userAvatarLink.$el
+ .querySelector('.js-user-avatar-link-username')
+ .getAttribute('tooltip-placement'),
).toEqual(this.propsData.tooltipPlacement);
});
});
diff --git a/spec/lib/banzai/filter/autolink_filter_spec.rb b/spec/lib/banzai/filter/autolink_filter_spec.rb
index a50329473ad..7a457403b51 100644
--- a/spec/lib/banzai/filter/autolink_filter_spec.rb
+++ b/spec/lib/banzai/filter/autolink_filter_spec.rb
@@ -76,7 +76,7 @@ describe Banzai::Filter::AutolinkFilter do
expect(doc.at_css('a')['href']).to eq link
end
- it 'autolinks multiple occurences of smb' do
+ it 'autolinks multiple occurrences of smb' do
link1 = 'smb:///Volumes/shared/foo.pdf'
link2 = 'smb:///Volumes/shared/bar.pdf'
diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
index 0d0554a2259..a0270d93d50 100644
--- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
@@ -101,15 +101,24 @@ describe Banzai::Filter::ExternalIssueReferenceFilter do
context "redmine project" do
let(:project) { create(:redmine_project) }
- let(:issue) { ExternalIssue.new("#123", project) }
- let(:reference) { issue.to_reference }
before do
- project.issues_enabled = false
- project.save!
+ project.update!(issues_enabled: false)
+ end
+
+ context "with a hash prefix" do
+ let(:issue) { ExternalIssue.new("#123", project) }
+ let(:reference) { issue.to_reference }
+
+ it_behaves_like "external issue tracker"
end
- it_behaves_like "external issue tracker"
+ context "with a single-letter prefix" do
+ let(:issue) { ExternalIssue.new("T-123", project) }
+ let(:reference) { issue.to_reference }
+
+ it_behaves_like "external issue tracker"
+ end
end
context "jira project" do
@@ -122,6 +131,15 @@ describe Banzai::Filter::ExternalIssueReferenceFilter do
it_behaves_like "external issue tracker"
end
+ context "with a single-letter prefix" do
+ let(:issue) { ExternalIssue.new("J-123", project) }
+
+ it "ignores reference" do
+ exp = act = "Issue #{reference}"
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+
context "with wrong markdown" do
let(:issue) { ExternalIssue.new("#123", project) }
diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb
index ed1ebe9ebf6..415ded05e6e 100644
--- a/spec/lib/banzai/filter/relative_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb
@@ -226,7 +226,7 @@ describe Banzai::Filter::RelativeLinkFilter do
let(:ref) {'mark#\'@],+;-._/#@!$&()+down'}
it 'correctly escapes the ref' do
- # Adressable won't escape the '#', so we do this manually
+ # Addressable won't escape the '#', so we do this manually
ref_escaped = 'mark%23\'@%5D,+;-._/%23@!$&()+down'
# Stub this method so the branch doesn't actually need to be in the repo
diff --git a/spec/lib/bitbucket_server/client_spec.rb b/spec/lib/bitbucket_server/client_spec.rb
index f926ae963a4..5de0a9a65b5 100644
--- a/spec/lib/bitbucket_server/client_spec.rb
+++ b/spec/lib/bitbucket_server/client_spec.rb
@@ -13,7 +13,7 @@ describe BitbucketServer::Client do
let(:path) { "/projects/#{project}/repos/#{repo_slug}/pull-requests?state=ALL" }
it 'requests a collection' do
- expect(BitbucketServer::Paginator).to receive(:new).with(anything, path, :pull_request)
+ expect(BitbucketServer::Paginator).to receive(:new).with(anything, path, :pull_request, page_offset: 0, limit: nil)
subject.pull_requests(project, repo_slug)
end
@@ -29,7 +29,7 @@ describe BitbucketServer::Client do
let(:path) { "/projects/#{project}/repos/#{repo_slug}/pull-requests/1/activities" }
it 'requests a collection' do
- expect(BitbucketServer::Paginator).to receive(:new).with(anything, path, :activity)
+ expect(BitbucketServer::Paginator).to receive(:new).with(anything, path, :activity, page_offset: 0, limit: nil)
subject.activities(project, repo_slug, 1)
end
@@ -52,10 +52,16 @@ describe BitbucketServer::Client do
let(:path) { "/repos" }
it 'requests a collection' do
- expect(BitbucketServer::Paginator).to receive(:new).with(anything, path, :repo)
+ expect(BitbucketServer::Paginator).to receive(:new).with(anything, path, :repo, page_offset: 0, limit: nil)
subject.repos
end
+
+ it 'requests a collection with an offset and limit' do
+ expect(BitbucketServer::Paginator).to receive(:new).with(anything, path, :repo, page_offset: 10, limit: 25)
+
+ subject.repos(page_offset: 10, limit: 25)
+ end
end
describe '#create_branch' do
diff --git a/spec/lib/bitbucket_server/collection_spec.rb b/spec/lib/bitbucket_server/collection_spec.rb
new file mode 100644
index 00000000000..ddd02bac88a
--- /dev/null
+++ b/spec/lib/bitbucket_server/collection_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe BitbucketServer::Collection do
+ let(:connection) { instance_double(BitbucketServer::Connection) }
+ let(:page) { 1 }
+ let(:paginator) { BitbucketServer::Paginator.new(connection, 'http://more-data', :pull_request, page_offset: page) }
+
+ subject { described_class.new(paginator) }
+
+ describe '#current_page' do
+ it 'returns 1' do
+ expect(subject.current_page).to eq(1)
+ end
+ end
+
+ describe '#prev_page' do
+ it 'returns nil' do
+ expect(subject.prev_page).to be_nil
+ end
+ end
+
+ describe '#next_page' do
+ it 'returns 2' do
+ expect(subject.next_page).to eq(2)
+ end
+ end
+end
diff --git a/spec/lib/bitbucket_server/paginator_spec.rb b/spec/lib/bitbucket_server/paginator_spec.rb
index 2de50eba3c4..d268d4f23cf 100644
--- a/spec/lib/bitbucket_server/paginator_spec.rb
+++ b/spec/lib/bitbucket_server/paginator_spec.rb
@@ -20,6 +20,16 @@ describe BitbucketServer::Paginator do
expect { paginator.items }.to raise_error(StopIteration)
end
+ it 'obeys limits' do
+ limited = described_class.new(connection, 'http://more-data', :pull_request, page_offset: 0, limit: 1)
+ allow(limited).to receive(:fetch_next_page).and_return(first_page)
+
+ expect(limited.has_next_page?).to be_truthy
+ expect(limited.items).to match(['item_1'])
+ expect(limited.has_next_page?).to be_truthy
+ expect { limited.items }.to raise_error(StopIteration)
+ end
+
it 'calls the connection with different offsets' do
expect(connection).to receive(:get).with('http://more-data', start: 0, limit: BitbucketServer::Paginator::PAGE_LENGTH).and_return(page_attrs)
diff --git a/spec/lib/container_registry/blob_spec.rb b/spec/lib/container_registry/blob_spec.rb
index c73faa55513..d3fff5bad42 100644
--- a/spec/lib/container_registry/blob_spec.rb
+++ b/spec/lib/container_registry/blob_spec.rb
@@ -64,7 +64,7 @@ describe ContainerRegistry::Blob do
.to_return(status: 200)
end
- it 'returns true when blob has been successfuly deleted' do
+ it 'returns true when blob has been successfully deleted' do
expect(blob.delete).to be_truthy
end
end
diff --git a/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb
index e1c4f9cfea7..5076996474f 100644
--- a/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb
+++ b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb
@@ -118,7 +118,7 @@ describe Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange, :migrat
expect(fork_network_members.count).to eq(12)
end
- it 'knows when not all memberships withing a batch have been created' do
+ it 'knows when not all memberships within a batch have been created' do
expect(migration.missing_members?(8, 10)).to be_truthy
end
end
diff --git a/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb b/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb
new file mode 100644
index 00000000000..4f1b01eed41
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::PopulateClusterKubernetesNamespaceTable, :migration, schema: 20181022173835 do
+ let(:migration) { described_class.new }
+ let(:clusters) { create_list(:cluster, 10, :project, :provided_by_gcp) }
+
+ before do
+ clusters
+ end
+
+ shared_examples 'consistent kubernetes namespace attributes' do
+ it 'should populate namespace and service account information' do
+ subject
+
+ clusters_with_namespace.each do |cluster|
+ project = cluster.project
+ cluster_project = cluster.cluster_projects.first
+ namespace = "#{project.path}-#{project.id}"
+ kubernetes_namespace = cluster.reload.kubernetes_namespace
+
+ expect(kubernetes_namespace).to be_present
+ expect(kubernetes_namespace.cluster_project).to eq(cluster_project)
+ expect(kubernetes_namespace.project).to eq(cluster_project.project)
+ expect(kubernetes_namespace.cluster).to eq(cluster_project.cluster)
+ expect(kubernetes_namespace.namespace).to eq(namespace)
+ expect(kubernetes_namespace.service_account_name).to eq("#{namespace}-service-account")
+ end
+ end
+ end
+
+ subject { migration.perform }
+
+ context 'when no Clusters::Project has a Clusters::KubernetesNamespace' do
+ let(:cluster_projects) { Clusters::Project.all }
+
+ it 'should create a Clusters::KubernetesNamespace per Clusters::Project' do
+ expect do
+ subject
+ end.to change(Clusters::KubernetesNamespace, :count).by(cluster_projects.count)
+ end
+
+ it_behaves_like 'consistent kubernetes namespace attributes' do
+ let(:clusters_with_namespace) { clusters }
+ end
+ end
+
+ context 'when every Clusters::Project has Clusters::KubernetesNamespace' do
+ before do
+ clusters.each do |cluster|
+ create(:cluster_kubernetes_namespace,
+ cluster_project: cluster.cluster_projects.first,
+ cluster: cluster,
+ project: cluster.project)
+ end
+ end
+
+ it 'should not create any Clusters::KubernetesNamespace' do
+ expect do
+ subject
+ end.not_to change(Clusters::KubernetesNamespace, :count)
+ end
+ end
+
+ context 'when only some Clusters::Project have Clusters::KubernetesNamespace related' do
+ let(:with_kubernetes_namespace) { clusters.first(6) }
+ let(:with_no_kubernetes_namespace) { clusters.last(4) }
+
+ before do
+ with_kubernetes_namespace.each do |cluster|
+ create(:cluster_kubernetes_namespace,
+ cluster_project: cluster.cluster_projects.first,
+ cluster: cluster,
+ project: cluster.project)
+ end
+ end
+
+ it 'creates limited number of Clusters::KubernetesNamespace' do
+ expect do
+ subject
+ end.to change(Clusters::KubernetesNamespace, :count).by(with_no_kubernetes_namespace.count)
+ end
+
+ it 'should not modify clusters with Clusters::KubernetesNamespace' do
+ subject
+
+ with_kubernetes_namespace.each do |cluster|
+ expect(cluster.kubernetes_namespaces.count).to eq(1)
+ end
+ end
+
+ it_behaves_like 'consistent kubernetes namespace attributes' do
+ let(:clusters_with_namespace) { with_no_kubernetes_namespace }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/checks/lfs_integrity_spec.rb b/spec/lib/gitlab/checks/lfs_integrity_spec.rb
index 0488720cec8..887ea8fc1e0 100644
--- a/spec/lib/gitlab/checks/lfs_integrity_spec.rb
+++ b/spec/lib/gitlab/checks/lfs_integrity_spec.rb
@@ -68,7 +68,7 @@ describe Gitlab::Checks::LfsIntegrity do
expect(subject.objects_missing?).to be_truthy
end
- it 'is false parent project already conatins LFS objects for the fork' do
+ it 'is false parent project already contains LFS objects for the fork' do
lfs_object = create(:lfs_object, oid: blob_object.lfs_oid)
create(:lfs_objects_project, project: parent_project, lfs_object: lfs_object)
diff --git a/spec/lib/gitlab/ci/ansi2html_spec.rb b/spec/lib/gitlab/ci/ansi2html_spec.rb
index 7549e9941b6..5a5c071c639 100644
--- a/spec/lib/gitlab/ci/ansi2html_spec.rb
+++ b/spec/lib/gitlab/ci/ansi2html_spec.rb
@@ -7,7 +7,7 @@ describe Gitlab::Ci::Ansi2html do
expect(convert_html("Hello")).to eq('Hello')
end
- it "strips non-color-changing controll sequences" do
+ it "strips non-color-changing control sequences" do
expect(convert_html("Hello \e[2Kworld")).to eq('Hello world')
end
diff --git a/spec/lib/gitlab/ci/build/policy/variables_spec.rb b/spec/lib/gitlab/ci/build/policy/variables_spec.rb
index 854c4cb718c..c2c0742efc3 100644
--- a/spec/lib/gitlab/ci/build/policy/variables_spec.rb
+++ b/spec/lib/gitlab/ci/build/policy/variables_spec.rb
@@ -24,7 +24,7 @@ describe Gitlab::Ci::Build::Policy::Variables do
expect(policy).to be_satisfied_by(pipeline, seed)
end
- it 'is not satisfied by an overriden empty variable' do
+ it 'is not satisfied by an overridden empty variable' do
policy = described_class.new(['$CI_PROJECT_NAME'])
expect(policy).not_to be_satisfied_by(pipeline, seed)
diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb
index 1860ed79bfd..7c18514934e 100644
--- a/spec/lib/gitlab/ci/config/entry/global_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb
@@ -219,7 +219,7 @@ describe Gitlab::Ci::Config::Entry::Global do
##
# When nodes are specified but not defined, we assume that
- # configuration is valid, and we asume that entry is simply undefined,
+ # configuration is valid, and we assume that entry is simply undefined,
# despite the fact, that key is present. See issue #18775 for more
# details.
#
diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index 1169938b80c..57d4577a90c 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -1,5 +1,4 @@
-require 'fast_spec_helper'
-require_dependency 'active_model'
+require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Job do
let(:entry) { described_class.new(config, name: :rspec) }
@@ -11,7 +10,7 @@ describe Gitlab::Ci::Config::Entry::Job do
let(:result) do
%i[before_script script stage type after_script cache
image services only except variables artifacts
- environment coverage]
+ environment coverage retry]
end
it { is_expected.to match_array result }
@@ -99,41 +98,42 @@ describe Gitlab::Ci::Config::Entry::Job do
end
end
- context 'when retry value is not correct' do
+ context 'when parallel value is not correct' do
context 'when it is not a numeric value' do
- let(:config) { { retry: true } }
+ let(:config) { { parallel: true } }
it 'returns error about invalid type' do
expect(entry).not_to be_valid
- expect(entry.errors).to include 'job retry is not a number'
+ expect(entry.errors).to include 'job parallel is not a number'
end
end
- context 'when it is lower than zero' do
- let(:config) { { retry: -1 } }
+ context 'when it is lower than two' do
+ let(:config) { { parallel: 1 } }
it 'returns error about value too low' do
expect(entry).not_to be_valid
expect(entry.errors)
- .to include 'job retry must be greater than or equal to 0'
+ .to include 'job parallel must be greater than or equal to 2'
end
end
- context 'when it is not an integer' do
- let(:config) { { retry: 1.5 } }
+ context 'when it is bigger than 50' do
+ let(:config) { { parallel: 51 } }
- it 'returns error about wrong value' do
+ it 'returns error about value too high' do
expect(entry).not_to be_valid
- expect(entry.errors).to include 'job retry must be an integer'
+ expect(entry.errors)
+ .to include 'job parallel must be less than or equal to 50'
end
end
- context 'when the value is too high' do
- let(:config) { { retry: 10 } }
+ context 'when it is not an integer' do
+ let(:config) { { parallel: 1.5 } }
- it 'returns error about value too high' do
+ it 'returns error about wrong value' do
expect(entry).not_to be_valid
- expect(entry.errors).to include 'job retry must be less than or equal to 2'
+ expect(entry.errors).to include 'job parallel must be an integer'
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/reports_spec.rb b/spec/lib/gitlab/ci/config/entry/reports_spec.rb
index 8095a231cf3..1140bfdf6c3 100644
--- a/spec/lib/gitlab/ci/config/entry/reports_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/reports_spec.rb
@@ -33,7 +33,7 @@ describe Gitlab::Ci::Config::Entry::Reports do
where(:keyword, :file) do
:junit | 'junit.xml'
- :codequality | 'codequality.json'
+ :codequality | 'gl-code-quality-report.json'
:sast | 'gl-sast-report.json'
:dependency_scanning | 'gl-dependency-scanning-report.json'
:container_scanning | 'gl-container-scanning-report.json'
diff --git a/spec/lib/gitlab/ci/config/entry/retry_spec.rb b/spec/lib/gitlab/ci/config/entry/retry_spec.rb
new file mode 100644
index 00000000000..164a9ed4c3d
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/retry_spec.rb
@@ -0,0 +1,236 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Retry do
+ let(:entry) { described_class.new(config) }
+
+ shared_context 'when retry value is a numeric', :numeric do
+ let(:config) { max }
+ let(:max) {}
+ end
+
+ shared_context 'when retry value is a hash', :hash do
+ let(:config) { { max: max, when: public_send(:when) }.compact }
+ let(:when) {}
+ let(:max) {}
+ end
+
+ describe '#value' do
+ subject(:value) { entry.value }
+
+ context 'when retry value is a numeric', :numeric do
+ let(:max) { 2 }
+
+ it 'is returned as a hash with max key' do
+ expect(value).to eq(max: 2)
+ end
+ end
+
+ context 'when retry value is a hash', :hash do
+ context 'and `when` is a string' do
+ let(:when) { 'unknown_failure' }
+
+ it 'returns when wrapped in an array' do
+ expect(value).to eq(when: ['unknown_failure'])
+ end
+ end
+
+ context 'and `when` is an array' do
+ let(:when) { %w[unknown_failure runner_system_failure] }
+
+ it 'returns when as it was passed' do
+ expect(value).to eq(when: %w[unknown_failure runner_system_failure])
+ end
+ end
+ end
+ end
+
+ describe 'validation' do
+ context 'when retry value is correct' do
+ context 'when it is a numeric', :numeric do
+ let(:max) { 2 }
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ context 'when it is a hash', :hash do
+ context 'with max' do
+ let(:max) { 2 }
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ context 'with string when' do
+ let(:when) { 'unknown_failure' }
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ context 'with string when always' do
+ let(:when) { 'always' }
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ context 'with array when' do
+ let(:when) { %w[unknown_failure runner_system_failure] }
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ # Those values are documented at `doc/ci/yaml/README.md`. If any of
+ # those values gets invalid, documentation must be updated. To make
+ # sure this is catched, check explicitly that all of the documented
+ # values are valid. If they are not it means the documentation and this
+ # array must be updated.
+ RETRY_WHEN_IN_DOCUMENTATION = %w[
+ always
+ unknown_failure
+ script_failure
+ api_failure
+ stuck_or_timeout_failure
+ runner_system_failure
+ missing_dependency_failure
+ runner_unsupported
+ ].freeze
+
+ RETRY_WHEN_IN_DOCUMENTATION.each do |reason|
+ context "with when from documentation `#{reason}`" do
+ let(:when) { reason }
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ ::Ci::Build.failure_reasons.each_key do |reason|
+ context "with when from CommitStatus.failure_reasons `#{reason}`" do
+ let(:when) { reason }
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+ end
+ end
+
+ context 'when retry value is not correct' do
+ context 'when it is not a numeric nor an array' do
+ let(:config) { true }
+
+ it 'returns error about invalid type' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'retry config has to be either an integer or a hash'
+ end
+ end
+
+ context 'when it is a numeric', :numeric do
+ context 'when it is lower than zero' do
+ let(:max) { -1 }
+
+ it 'returns error about value too low' do
+ expect(entry).not_to be_valid
+ expect(entry.errors)
+ .to include 'retry config must be greater than or equal to 0'
+ end
+ end
+
+ context 'when it is not an integer' do
+ let(:max) { 1.5 }
+
+ it 'returns error about wrong value' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'retry config has to be either an integer or a hash'
+ end
+ end
+
+ context 'when the value is too high' do
+ let(:max) { 10 }
+
+ it 'returns error about value too high' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'retry config must be less than or equal to 2'
+ end
+ end
+ end
+
+ context 'when it is a hash', :hash do
+ context 'with unknown keys' do
+ let(:config) { { max: 2, unknown_key: :something, one_more: :key } }
+
+ it 'returns error about the unknown key' do
+ expect(entry).not_to be_valid
+ expect(entry.errors)
+ .to include 'retry config contains unknown keys: unknown_key, one_more'
+ end
+ end
+
+ context 'with max lower than zero' do
+ let(:max) { -1 }
+
+ it 'returns error about value too low' do
+ expect(entry).not_to be_valid
+ expect(entry.errors)
+ .to include 'retry max must be greater than or equal to 0'
+ end
+ end
+
+ context 'with max not an integer' do
+ let(:max) { 1.5 }
+
+ it 'returns error about wrong value' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'retry max must be an integer'
+ end
+ end
+
+ context 'iwth max too high' do
+ let(:max) { 10 }
+
+ it 'returns error about value too high' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'retry max must be less than or equal to 2'
+ end
+ end
+
+ context 'with when in wrong format' do
+ let(:when) { true }
+
+ it 'returns error about the wrong format' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'retry when should be an array of strings or a string'
+ end
+ end
+
+ context 'with an unknown when string' do
+ let(:when) { 'unknown_reason' }
+
+ it 'returns error about the wrong format' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'retry when is not included in the list'
+ end
+ end
+
+ context 'with an unknown failure reason in a when array' do
+ let(:when) { %w[unknown_reason runner_system_failure] }
+
+ it 'returns error about the wrong format' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'retry when contains unknown values: unknown_reason'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/normalizer_spec.rb b/spec/lib/gitlab/ci/config/normalizer_spec.rb
new file mode 100644
index 00000000000..97926695b6e
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/normalizer_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+describe Gitlab::Ci::Config::Normalizer do
+ let(:job_name) { :rspec }
+ let(:job_config) { { script: 'rspec', parallel: 5, name: 'rspec' } }
+ let(:config) { { job_name => job_config } }
+
+ describe '.normalize_jobs' do
+ subject { described_class.new(config).normalize_jobs }
+
+ it 'does not have original job' do
+ is_expected.not_to include(job_name)
+ end
+
+ it 'has parallelized jobs' do
+ job_names = [:"rspec 1/5", :"rspec 2/5", :"rspec 3/5", :"rspec 4/5", :"rspec 5/5"]
+
+ is_expected.to include(*job_names)
+ end
+
+ it 'sets job instance in options' do
+ expect(subject.values).to all(include(:instance))
+ end
+
+ it 'parallelizes jobs with original config' do
+ original_config = config[job_name].except(:name)
+ configs = subject.values.map { |config| config.except(:name, :instance) }
+
+ expect(configs).to all(eq(original_config))
+ end
+
+ context 'when the job is not parallelized' do
+ let(:job_config) { { script: 'rspec', name: 'rspec' } }
+
+ it 'returns the same hash' do
+ is_expected.to eq(config)
+ end
+ end
+
+ context 'when there is a job with a slash in it' do
+ let(:job_name) { :"rspec 35/2" }
+
+ it 'properly parallelizes job names' do
+ job_names = [:"rspec 35/2 1/5", :"rspec 35/2 2/5", :"rspec 35/2 3/5", :"rspec 35/2 4/5", :"rspec 35/2 5/5"]
+
+ is_expected.to include(*job_names)
+ end
+ end
+
+ context 'when jobs depend on parallelized jobs' do
+ let(:config) { { job_name => job_config, other_job: { script: 'echo 1', dependencies: [job_name.to_s] } } }
+
+ it 'parallelizes dependencies' do
+ job_names = ["rspec 1/5", "rspec 2/5", "rspec 3/5", "rspec 4/5", "rspec 5/5"]
+
+ expect(subject[:other_job][:dependencies]).to include(*job_names)
+ end
+
+ it 'does not include original job name in dependencies' do
+ expect(subject[:other_job][:dependencies]).not_to include(job_name)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/build/scheduled_spec.rb b/spec/lib/gitlab/ci/status/build/scheduled_spec.rb
index 4a52b3ab8de..68b87fea75d 100644
--- a/spec/lib/gitlab/ci/status/build/scheduled_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/scheduled_spec.rb
@@ -13,24 +13,10 @@ describe Gitlab::Ci::Status::Build::Scheduled do
end
describe '#status_tooltip' do
- context 'when scheduled_at is not expired' do
- let(:build) { create(:ci_build, scheduled_at: 1.minute.since, project: project) }
-
- it 'shows execute_in of the scheduled job' do
- Timecop.freeze(Time.now.change(usec: 0)) do
- expect(subject.status_tooltip).to include('00:01:00')
- end
- end
- end
-
- context 'when scheduled_at is expired' do
- let(:build) { create(:ci_build, :expired_scheduled, project: project) }
+ let(:build) { create(:ci_build, scheduled_at: 1.minute.since, project: project) }
- it 'shows 00:00' do
- Timecop.freeze do
- expect(subject.status_tooltip).to include('00:00')
- end
- end
+ it 'has a placeholder for the remaining time' do
+ expect(subject.status_tooltip).to include('%{remainingTime}')
end
end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index 85b23edce9f..441e8214181 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -53,11 +53,11 @@ module Gitlab
describe 'retry entry' do
context 'when retry count is specified' do
let(:config) do
- YAML.dump(rspec: { script: 'rspec', retry: 1 })
+ YAML.dump(rspec: { script: 'rspec', retry: { max: 1 } })
end
it 'includes retry count in build options attribute' do
- expect(subject[:options]).to include(retry: 1)
+ expect(subject[:options]).to include(retry: { max: 1 })
end
end
@@ -645,6 +645,33 @@ module Gitlab
end
end
+ describe 'Parallel' do
+ context 'when job is parallelized' do
+ let(:parallel) { 5 }
+
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec',
+ parallel: parallel })
+ end
+
+ it 'returns parallelized jobs' do
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+ builds = config_processor.stage_builds_attributes('test')
+ build_options = builds.map { |build| build[:options] }
+
+ expect(builds.size).to eq(5)
+ expect(build_options).to all(include(:instance, parallel: parallel))
+ end
+
+ it 'does not have the original job' do
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+ builds = config_processor.stage_builds_attributes('test')
+
+ expect(builds).not_to include(:rspec)
+ end
+ end
+ end
+
describe 'cache' do
context 'when cache definition has unknown keys' do
it 'raises relevant validation error' do
diff --git a/spec/lib/gitlab/cross_project_access/check_info_spec.rb b/spec/lib/gitlab/cross_project_access/check_info_spec.rb
index bc9dbf2bece..239fa364f5e 100644
--- a/spec/lib/gitlab/cross_project_access/check_info_spec.rb
+++ b/spec/lib/gitlab/cross_project_access/check_info_spec.rb
@@ -50,7 +50,7 @@ describe Gitlab::CrossProjectAccess::CheckInfo do
expect(info.should_run?(dummy_controller)).to be_truthy
end
- it 'returns the the oposite of #should_skip? when the check is a skip' do
+ it 'returns the the opposite of #should_skip? when the check is a skip' do
info = described_class.new({}, nil, nil, true)
expect(info).to receive(:should_skip?).with(dummy_controller).and_return(false)
@@ -101,7 +101,7 @@ describe Gitlab::CrossProjectAccess::CheckInfo do
expect(info.should_skip?(dummy_controller)).to be_truthy
end
- it 'returns the the oposite of #should_run? when the check is not a skip' do
+ it 'returns the the opposite of #should_run? when the check is not a skip' do
info = described_class.new({}, nil, nil, false)
expect(info).to receive(:should_run?).with(dummy_controller).and_return(false)
diff --git a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
index 2e67c1c7f78..f8009709ce2 100644
--- a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
@@ -44,15 +44,15 @@ describe Gitlab::CycleAnalytics::StageSummary do
describe "#deploys" do
it "finds the number of deploys made created after the 'from date'" do
- Timecop.freeze(5.days.ago) { create(:deployment, project: project) }
- Timecop.freeze(5.days.from_now) { create(:deployment, project: project) }
+ Timecop.freeze(5.days.ago) { create(:deployment, :success, project: project) }
+ Timecop.freeze(5.days.from_now) { create(:deployment, :success, project: project) }
expect(subject.third[:value]).to eq(1)
end
it "doesn't find commits from other projects" do
Timecop.freeze(5.days.from_now) do
- create(:deployment, project: create(:project, :repository))
+ create(:deployment, :success, project: create(:project, :repository))
end
expect(subject.third[:value]).to eq(0)
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 4e83b27e4a5..23f27939dd2 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -1338,7 +1338,12 @@ describe Gitlab::Database::MigrationHelpers do
end
describe '#index_exists_by_name?' do
- it 'returns true if an index exists' do
+ # TODO: remove rails5-only after removing rails4 tests
+ # rails 4 can not handle multiple indexes on the same column set if
+ # index was added by 't.index' - t.index is used by default in schema.rb in
+ # rails 5. Let's run this test only in rails 5 env:
+ # see https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21492#note_113602758
+ it 'returns true if an index exists', :rails5 do
expect(model.index_exists_by_name?(:projects, 'index_projects_on_path'))
.to be_truthy
end
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb
index cc7cb3f23fd..248cca25a2c 100644
--- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb
@@ -20,7 +20,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :delete
end
describe "#remove_last_ocurrence" do
- it "removes only the last occurance of a string" do
+ it "removes only the last occurrence of a string" do
input = "this/is/a-word-to-replace/namespace/with/a-word-to-replace"
expect(subject.remove_last_occurrence(input, "a-word-to-replace"))
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index 2f51642b58e..3417896e259 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -41,6 +41,52 @@ describe Gitlab::Diff::File do
end
end
+ describe '#unfold_diff_lines' do
+ let(:unfolded_lines) { double('expanded-lines') }
+ let(:unfolder) { instance_double(Gitlab::Diff::LinesUnfolder) }
+ let(:position) { instance_double(Gitlab::Diff::Position, old_line: 10) }
+
+ before do
+ allow(Gitlab::Diff::LinesUnfolder).to receive(:new) { unfolder }
+ end
+
+ context 'when unfold required' do
+ before do
+ allow(unfolder).to receive(:unfold_required?) { true }
+ allow(unfolder).to receive(:unfolded_diff_lines) { unfolded_lines }
+ end
+
+ it 'changes @unfolded to true' do
+ diff_file.unfold_diff_lines(position)
+
+ expect(diff_file).to be_unfolded
+ end
+
+ it 'updates @diff_lines' do
+ diff_file.unfold_diff_lines(position)
+
+ expect(diff_file.diff_lines).to eq(unfolded_lines)
+ end
+ end
+
+ context 'when unfold not required' do
+ before do
+ allow(unfolder).to receive(:unfold_required?) { false }
+ end
+
+ it 'keeps @unfolded false' do
+ diff_file.unfold_diff_lines(position)
+
+ expect(diff_file).not_to be_unfolded
+ end
+
+ it 'does not update @diff_lines' do
+ expect { diff_file.unfold_diff_lines(position) }
+ .not_to change(diff_file, :diff_lines)
+ end
+ end
+ end
+
describe '#mode_changed?' do
it { expect(diff_file.mode_changed?).to be_falsey }
end
diff --git a/spec/lib/gitlab/diff/lines_unfolder_spec.rb b/spec/lib/gitlab/diff/lines_unfolder_spec.rb
new file mode 100644
index 00000000000..8e00c8e0e30
--- /dev/null
+++ b/spec/lib/gitlab/diff/lines_unfolder_spec.rb
@@ -0,0 +1,750 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Diff::LinesUnfolder do
+ let(:raw_diff) do
+ <<-DIFF.strip_heredoc
+ @@ -7,9 +7,6 @@
+ "tags": ["devel", "development", "nightly"],
+ "desktop-file-name-prefix": "(Development) ",
+ "finish-args": [
+ - "--share=ipc", "--socket=x11",
+ - "--socket=wayland",
+ - "--talk-name=org.gnome.OnlineAccounts",
+ "--talk-name=org.freedesktop.Tracker1",
+ "--filesystem=home",
+ "--talk-name=org.gtk.vfs", "--talk-name=org.gtk.vfs.*",
+ @@ -62,7 +59,7 @@
+ },
+ {
+ "name": "gnome-desktop",
+ - "config-opts": ["--disable-debug-tools", "--disable-udev"],
+ + "config-opts": ["--disable-debug-tools", "--disable-"],
+ "sources": [
+ {
+ "type": "git",
+ @@ -83,11 +80,6 @@
+ "buildsystem": "meson",
+ "builddir": true,
+ "name": "nautilus",
+ - "config-opts": [
+ - "-Denable-desktop=false",
+ - "-Denable-selinux=false",
+ - "--libdir=/app/lib"
+ - ],
+ "sources": [
+ {
+ "type": "git",
+ DIFF
+ end
+
+ let(:raw_old_blob) do
+ <<-BLOB.strip_heredoc
+ {
+ "app-id": "org.gnome.Nautilus",
+ "runtime": "org.gnome.Platform",
+ "runtime-version": "master",
+ "sdk": "org.gnome.Sdk",
+ "command": "nautilus",
+ "tags": ["devel", "development", "nightly"],
+ "desktop-file-name-prefix": "(Development) ",
+ "finish-args": [
+ "--share=ipc", "--socket=x11",
+ "--socket=wayland",
+ "--talk-name=org.gnome.OnlineAccounts",
+ "--talk-name=org.freedesktop.Tracker1",
+ "--filesystem=home",
+ "--talk-name=org.gtk.vfs", "--talk-name=org.gtk.vfs.*",
+ "--filesystem=xdg-run/dconf", "--filesystem=~/.config/dconf:ro",
+ "--talk-name=ca.desrt.dconf", "--env=DCONF_USER_CONFIG_DIR=.config/dconf"
+ ],
+ "cleanup": [ "/include", "/share/bash-completion" ],
+ "modules": [
+ {
+ "name": "exiv2",
+ "sources": [
+ {
+ "type": "archive",
+ "url": "http://exiv2.org/builds/exiv2-0.26-trunk.tar.gz",
+ "sha256": "c75e3c4a0811bf700d92c82319373b7a825a2331c12b8b37d41eb58e4f18eafb"
+ },
+ {
+ "type": "shell",
+ "commands": [
+ "cp -f /usr/share/gnu-config/config.sub ./config/",
+ "cp -f /usr/share/gnu-config/config.guess ./config/"
+ ]
+ }
+ ]
+ },
+ {
+ "name": "gexiv2",
+ "config-opts": [ "--disable-introspection" ],
+ "sources": [
+ {
+ "type": "git",
+ "url": "https://git.gnome.org/browse/gexiv2"
+ }
+ ]
+ },
+ {
+ "name": "tracker",
+ "cleanup": [ "/bin", "/etc", "/libexec" ],
+ "config-opts": [ "--disable-miner-apps", "--disable-static",
+ "--disable-tracker-extract", "--disable-tracker-needle",
+ "--disable-tracker-preferences", "--disable-artwork",
+ "--disable-tracker-writeback", "--disable-miner-user-guides",
+ "--with-bash-completion-dir=no" ],
+ "sources": [
+ {
+ "type": "git",
+ "url": "https://git.gnome.org/browse/tracker"
+ }
+ ]
+ },
+ {
+ "name": "gnome-desktop",
+ "config-opts": ["--disable-debug-tools", "--disable-udev"],
+ "sources": [
+ {
+ "type": "git",
+ "url": "https://git.gnome.org/browse/gnome-desktop"
+ }
+ ]
+ },
+ {
+ "name": "gnome-autoar",
+ "sources": [
+ {
+ "type": "git",
+ "url": "https://git.gnome.org/browse/gnome-autoar"
+ }
+ ]
+ },
+ {
+ "buildsystem": "meson",
+ "builddir": true,
+ "name": "nautilus",
+ "config-opts": [
+ "-Denable-desktop=false",
+ "-Denable-selinux=false",
+ "--libdir=/app/lib"
+ ],
+ "sources": [
+ {
+ "type": "git",
+ "url": "https://gitlab.gnome.org/GNOME/nautilus.git"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "app-id": "foo",
+ "runtime": "foo",
+ "runtime-version": "foo",
+ "sdk": "foo",
+ "command": "foo",
+ "tags": ["foo", "bar", "kux"],
+ "desktop-file-name-prefix": "(Foo) ",
+ {
+ "buildsystem": "meson",
+ "builddir": true,
+ "name": "nautilus",
+ "sources": [
+ {
+ "type": "git",
+ "url": "https://gitlab.gnome.org/GNOME/nautilus.git"
+ }
+ ]
+ }
+ },
+ {
+ "app-id": "foo",
+ "runtime": "foo",
+ "runtime-version": "foo",
+ "sdk": "foo",
+ "command": "foo",
+ "tags": ["foo", "bar", "kux"],
+ "desktop-file-name-prefix": "(Foo) ",
+ {
+ "buildsystem": "meson",
+ "builddir": true,
+ "name": "nautilus",
+ "sources": [
+ {
+ "type": "git",
+ "url": "https://gitlab.gnome.org/GNOME/nautilus.git"
+ }
+ ]
+ }
+ }
+ BLOB
+ end
+
+ let(:project) { create(:project) }
+
+ let(:old_blob) { Gitlab::Git::Blob.new(data: raw_old_blob) }
+
+ let(:diff) do
+ Gitlab::Git::Diff.new(diff: raw_diff,
+ new_path: "build-aux/flatpak/org.gnome.Nautilus.json",
+ old_path: "build-aux/flatpak/org.gnome.Nautilus.json",
+ a_mode: "100644",
+ b_mode: "100644",
+ new_file: false,
+ renamed_file: false,
+ deleted_file: false,
+ too_large: false)
+ end
+
+ let(:diff_file) do
+ Gitlab::Diff::File.new(diff, repository: project.repository)
+ end
+
+ before do
+ allow(old_blob).to receive(:load_all_data!)
+ allow(diff_file).to receive(:old_blob) { old_blob }
+ end
+
+ subject { described_class.new(diff_file, position) }
+
+ context 'position requires a middle expansion and new match lines' do
+ let(:position) do
+ Gitlab::Diff::Position.new(base_sha: "1c59dfa64afbea8c721bb09a06a9d326c952ea19",
+ start_sha: "1c59dfa64afbea8c721bb09a06a9d326c952ea19",
+ head_sha: "1487062132228de836236c522fe52fed4980a46c",
+ old_path: "build-aux/flatpak/org.gnome.Nautilus.json",
+ new_path: "build-aux/flatpak/org.gnome.Nautilus.json",
+ position_type: "text",
+ old_line: 43,
+ new_line: 40)
+ end
+
+ context 'blob lines' do
+ let(:expected_blob_lines) do
+ [[40, 40, " \"config-opts\": [ \"--disable-introspection\" ],"],
+ [41, 41, " \"sources\": ["],
+ [42, 42, " {"],
+ [43, 43, " \"type\": \"git\","],
+ [44, 44, " \"url\": \"https://git.gnome.org/browse/gexiv2\""],
+ [45, 45, " }"],
+ [46, 46, " ]"]]
+ end
+
+ it 'returns the extracted blob lines correctly' do
+ extracted_lines = subject.blob_lines
+
+ expect(extracted_lines.size).to eq(7)
+
+ extracted_lines.each_with_index do |line, i|
+ expect([line.old_line, line.new_line, line.text]).to eq(expected_blob_lines[i])
+ end
+ end
+ end
+
+ context 'diff lines' do
+ let(:expected_diff_lines) do
+ [[7, 7, "@@ -7,9 +7,6 @@"],
+ [7, 7, " \"tags\": [\"devel\", \"development\", \"nightly\"],"],
+ [8, 8, " \"desktop-file-name-prefix\": \"(Development) \","],
+ [9, 9, " \"finish-args\": ["],
+ [10, 10, "- \"--share=ipc\", \"--socket=x11\","],
+ [11, 10, "- \"--socket=wayland\","],
+ [12, 10, "- \"--talk-name=org.gnome.OnlineAccounts\","],
+ [13, 10, " \"--talk-name=org.freedesktop.Tracker1\","],
+ [14, 11, " \"--filesystem=home\","],
+ [15, 12, " \"--talk-name=org.gtk.vfs\", \"--talk-name=org.gtk.vfs.*\","],
+
+ # New match line
+ [40, 37, "@@ -40,7+37,7 @@"],
+
+ # Injected blob lines
+ [40, 37, " \"config-opts\": [ \"--disable-introspection\" ],"],
+ [41, 38, " \"sources\": ["],
+ [42, 39, " {"],
+ [43, 40, " \"type\": \"git\","], # comment
+ [44, 41, " \"url\": \"https://git.gnome.org/browse/gexiv2\""],
+ [45, 42, " }"],
+ [46, 43, " ]"],
+ # end
+
+ # Second match line
+ [62, 59, "@@ -62,7+59,7 @@"],
+
+ [62, 59, " },"],
+ [63, 60, " {"],
+ [64, 61, " \"name\": \"gnome-desktop\","],
+ [65, 62, "- \"config-opts\": [\"--disable-debug-tools\", \"--disable-udev\"],"],
+ [66, 62, "+ \"config-opts\": [\"--disable-debug-tools\", \"--disable-\"],"],
+ [66, 63, " \"sources\": ["],
+ [67, 64, " {"],
+ [68, 65, " \"type\": \"git\","],
+ [83, 80, "@@ -83,11 +80,6 @@"],
+ [83, 80, " \"buildsystem\": \"meson\","],
+ [84, 81, " \"builddir\": true,"],
+ [85, 82, " \"name\": \"nautilus\","],
+ [86, 83, "- \"config-opts\": ["],
+ [87, 83, "- \"-Denable-desktop=false\","],
+ [88, 83, "- \"-Denable-selinux=false\","],
+ [89, 83, "- \"--libdir=/app/lib\""],
+ [90, 83, "- ],"],
+ [91, 83, " \"sources\": ["],
+ [92, 84, " {"],
+ [93, 85, " \"type\": \"git\","]]
+ end
+
+ it 'return merge of blob lines with diff lines correctly' do
+ new_diff_lines = subject.unfolded_diff_lines
+
+ expected_diff_lines.each_with_index do |expected_line, i|
+ line = new_diff_lines[i]
+
+ expect([line.old_pos, line.new_pos, line.text])
+ .to eq([expected_line[0], expected_line[1], expected_line[2]])
+ end
+ end
+
+ it 'merged lines have correct line codes' do
+ new_diff_lines = subject.unfolded_diff_lines
+
+ new_diff_lines.each_with_index do |line, i|
+ old_pos, new_pos = expected_diff_lines[i][0], expected_diff_lines[i][1]
+
+ unless line.type == 'match'
+ expect(line.line_code).to eq(Gitlab::Git.diff_line_code(diff_file.file_path, new_pos, old_pos))
+ end
+ end
+ end
+ end
+ end
+
+ context 'position requires a middle expansion and no top match line' do
+ let(:position) do
+ Gitlab::Diff::Position.new(base_sha: "1c59dfa64afbea8c721bb09a06a9d326c952ea19",
+ start_sha: "1c59dfa64afbea8c721bb09a06a9d326c952ea19",
+ head_sha: "1487062132228de836236c522fe52fed4980a46c",
+ old_path: "build-aux/flatpak/org.gnome.Nautilus.json",
+ new_path: "build-aux/flatpak/org.gnome.Nautilus.json",
+ position_type: "text",
+ old_line: 16,
+ new_line: 17)
+ end
+
+ context 'blob lines' do
+ let(:expected_blob_lines) do
+ [[16, 16, " \"--filesystem=xdg-run/dconf\", \"--filesystem=~/.config/dconf:ro\","],
+ [17, 17, " \"--talk-name=ca.desrt.dconf\", \"--env=DCONF_USER_CONFIG_DIR=.config/dconf\""],
+ [18, 18, " ],"],
+ [19, 19, " \"cleanup\": [ \"/include\", \"/share/bash-completion\" ],"]]
+ end
+
+ it 'returns the extracted blob lines correctly' do
+ extracted_lines = subject.blob_lines
+
+ expect(extracted_lines.size).to eq(4)
+
+ extracted_lines.each_with_index do |line, i|
+ expect([line.old_line, line.new_line, line.text]).to eq(expected_blob_lines[i])
+ end
+ end
+ end
+
+ context 'diff lines' do
+ let(:expected_diff_lines) do
+ [[7, 7, "@@ -7,9 +7,6 @@"],
+ [7, 7, " \"tags\": [\"devel\", \"development\", \"nightly\"],"],
+ [8, 8, " \"desktop-file-name-prefix\": \"(Development) \","],
+ [9, 9, " \"finish-args\": ["],
+ [10, 10, "- \"--share=ipc\", \"--socket=x11\","],
+ [11, 10, "- \"--socket=wayland\","],
+ [12, 10, "- \"--talk-name=org.gnome.OnlineAccounts\","],
+ [13, 10, " \"--talk-name=org.freedesktop.Tracker1\","],
+ [14, 11, " \"--filesystem=home\","],
+ [15, 12, " \"--talk-name=org.gtk.vfs\", \"--talk-name=org.gtk.vfs.*\","],
+ # No new match needed
+
+ # Injected blob lines
+ [16, 13, " \"--filesystem=xdg-run/dconf\", \"--filesystem=~/.config/dconf:ro\","],
+ [17, 14, " \"--talk-name=ca.desrt.dconf\", \"--env=DCONF_USER_CONFIG_DIR=.config/dconf\""],
+ [18, 15, " ],"],
+ [19, 16, " \"cleanup\": [ \"/include\", \"/share/bash-completion\" ],"],
+ # end
+
+ # Second match line
+ [62, 59, "@@ -62,4+59,4 @@"],
+
+ [62, 59, " },"],
+ [63, 60, " {"],
+ [64, 61, " \"name\": \"gnome-desktop\","],
+ [65, 62, "- \"config-opts\": [\"--disable-debug-tools\", \"--disable-udev\"],"],
+ [66, 62, "+ \"config-opts\": [\"--disable-debug-tools\", \"--disable-\"],"],
+ [66, 63, " \"sources\": ["],
+ [67, 64, " {"],
+ [68, 65, " \"type\": \"git\","],
+ [83, 80, "@@ -83,11 +80,6 @@"],
+ [83, 80, " \"buildsystem\": \"meson\","],
+ [84, 81, " \"builddir\": true,"],
+ [85, 82, " \"name\": \"nautilus\","],
+ [86, 83, "- \"config-opts\": ["],
+ [87, 83, "- \"-Denable-desktop=false\","],
+ [88, 83, "- \"-Denable-selinux=false\","],
+ [89, 83, "- \"--libdir=/app/lib\""],
+ [90, 83, "- ],"],
+ [91, 83, " \"sources\": ["],
+ [92, 84, " {"],
+ [93, 85, " \"type\": \"git\","]]
+ end
+
+ it 'return merge of blob lines with diff lines correctly' do
+ new_diff_lines = subject.unfolded_diff_lines
+
+ expected_diff_lines.each_with_index do |expected_line, i|
+ line = new_diff_lines[i]
+
+ expect([line.old_pos, line.new_pos, line.text])
+ .to eq([expected_line[0], expected_line[1], expected_line[2]])
+ end
+ end
+
+ it 'merged lines have correct line codes' do
+ new_diff_lines = subject.unfolded_diff_lines
+
+ new_diff_lines.each_with_index do |line, i|
+ old_pos, new_pos = expected_diff_lines[i][0], expected_diff_lines[i][1]
+
+ unless line.type == 'match'
+ expect(line.line_code).to eq(Gitlab::Git.diff_line_code(diff_file.file_path, new_pos, old_pos))
+ end
+ end
+ end
+ end
+ end
+
+ context 'position requires a middle expansion and no bottom match line' do
+ let(:position) do
+ Gitlab::Diff::Position.new(base_sha: "1c59dfa64afbea8c721bb09a06a9d326c952ea19",
+ start_sha: "1c59dfa64afbea8c721bb09a06a9d326c952ea19",
+ head_sha: "1487062132228de836236c522fe52fed4980a46c",
+ old_path: "build-aux/flatpak/org.gnome.Nautilus.json",
+ new_path: "build-aux/flatpak/org.gnome.Nautilus.json",
+ position_type: "text",
+ old_line: 82,
+ new_line: 79)
+ end
+
+ context 'blob lines' do
+ let(:expected_blob_lines) do
+ [[79, 79, " }"],
+ [80, 80, " ]"],
+ [81, 81, " },"],
+ [82, 82, " {"]]
+ end
+
+ it 'returns the extracted blob lines correctly' do
+ extracted_lines = subject.blob_lines
+
+ expect(extracted_lines.size).to eq(4)
+
+ extracted_lines.each_with_index do |line, i|
+ expect([line.old_line, line.new_line, line.text]).to eq(expected_blob_lines[i])
+ end
+ end
+ end
+
+ context 'diff lines' do
+ let(:expected_diff_lines) do
+ [[7, 7, "@@ -7,9 +7,6 @@"],
+ [7, 7, " \"tags\": [\"devel\", \"development\", \"nightly\"],"],
+ [8, 8, " \"desktop-file-name-prefix\": \"(Development) \","],
+ [9, 9, " \"finish-args\": ["],
+ [10, 10, "- \"--share=ipc\", \"--socket=x11\","],
+ [11, 10, "- \"--socket=wayland\","],
+ [12, 10, "- \"--talk-name=org.gnome.OnlineAccounts\","],
+ [13, 10, " \"--talk-name=org.freedesktop.Tracker1\","],
+ [14, 11, " \"--filesystem=home\","],
+ [15, 12, " \"--talk-name=org.gtk.vfs\", \"--talk-name=org.gtk.vfs.*\","],
+ [62, 59, "@@ -62,7 +59,7 @@"],
+ [62, 59, " },"],
+ [63, 60, " {"],
+ [64, 61, " \"name\": \"gnome-desktop\","],
+ [65, 62, "- \"config-opts\": [\"--disable-debug-tools\", \"--disable-udev\"],"],
+ [66, 62, "+ \"config-opts\": [\"--disable-debug-tools\", \"--disable-\"],"],
+ [66, 63, " \"sources\": ["],
+ [67, 64, " {"],
+ [68, 65, " \"type\": \"git\","],
+
+ # New top match line
+ [79, 76, "@@ -79,4+76,4 @@"],
+
+ # Injected blob lines
+ [79, 76, " }"],
+ [80, 77, " ]"],
+ [81, 78, " },"],
+ [82, 79, " {"],
+ # end
+
+ # No new second match line
+ [83, 80, " \"buildsystem\": \"meson\","],
+ [84, 81, " \"builddir\": true,"],
+ [85, 82, " \"name\": \"nautilus\","],
+ [86, 83, "- \"config-opts\": ["],
+ [87, 83, "- \"-Denable-desktop=false\","],
+ [88, 83, "- \"-Denable-selinux=false\","],
+ [89, 83, "- \"--libdir=/app/lib\""],
+ [90, 83, "- ],"],
+ [91, 83, " \"sources\": ["],
+ [92, 84, " {"],
+ [93, 85, " \"type\": \"git\","]]
+ end
+
+ it 'return merge of blob lines with diff lines correctly' do
+ new_diff_lines = subject.unfolded_diff_lines
+
+ expected_diff_lines.each_with_index do |expected_line, i|
+ line = new_diff_lines[i]
+
+ expect([line.old_pos, line.new_pos, line.text])
+ .to eq([expected_line[0], expected_line[1], expected_line[2]])
+ end
+ end
+
+ it 'merged lines have correct line codes' do
+ new_diff_lines = subject.unfolded_diff_lines
+
+ new_diff_lines.each_with_index do |line, i|
+ old_pos, new_pos = expected_diff_lines[i][0], expected_diff_lines[i][1]
+
+ unless line.type == 'match'
+ expect(line.line_code).to eq(Gitlab::Git.diff_line_code(diff_file.file_path, new_pos, old_pos))
+ end
+ end
+ end
+ end
+ end
+
+ context 'position requires a short top expansion' do
+ let(:position) do
+ Gitlab::Diff::Position.new(base_sha: "1c59dfa64afbea8c721bb09a06a9d326c952ea19",
+ start_sha: "1c59dfa64afbea8c721bb09a06a9d326c952ea19",
+ head_sha: "1487062132228de836236c522fe52fed4980a46c",
+ old_path: "build-aux/flatpak/org.gnome.Nautilus.json",
+ new_path: "build-aux/flatpak/org.gnome.Nautilus.json",
+ position_type: "text",
+ old_line: 6,
+ new_line: 6)
+ end
+
+ context 'blob lines' do
+ let(:expected_blob_lines) do
+ [[3, 3, " \"runtime\": \"org.gnome.Platform\","],
+ [4, 4, " \"runtime-version\": \"master\","],
+ [5, 5, " \"sdk\": \"org.gnome.Sdk\","],
+ [6, 6, " \"command\": \"nautilus\","]]
+ end
+
+ it 'returns the extracted blob lines correctly' do
+ extracted_lines = subject.blob_lines
+
+ expect(extracted_lines.size).to eq(4)
+
+ extracted_lines.each_with_index do |line, i|
+ expect([line.old_line, line.new_line, line.text]).to eq(expected_blob_lines[i])
+ end
+ end
+ end
+
+ context 'diff lines' do
+ let(:expected_diff_lines) do
+ # New match line
+ [[3, 3, "@@ -3,4+3,4 @@"],
+
+ # Injected blob lines
+ [3, 3, " \"runtime\": \"org.gnome.Platform\","],
+ [4, 4, " \"runtime-version\": \"master\","],
+ [5, 5, " \"sdk\": \"org.gnome.Sdk\","],
+ [6, 6, " \"command\": \"nautilus\","],
+ # end
+ [7, 7, " \"tags\": [\"devel\", \"development\", \"nightly\"],"],
+ [8, 8, " \"desktop-file-name-prefix\": \"(Development) \","],
+ [9, 9, " \"finish-args\": ["],
+ [10, 10, "- \"--share=ipc\", \"--socket=x11\","],
+ [11, 10, "- \"--socket=wayland\","],
+ [12, 10, "- \"--talk-name=org.gnome.OnlineAccounts\","],
+ [13, 10, " \"--talk-name=org.freedesktop.Tracker1\","],
+ [14, 11, " \"--filesystem=home\","],
+ [15, 12, " \"--talk-name=org.gtk.vfs\", \"--talk-name=org.gtk.vfs.*\","],
+ [62, 59, "@@ -62,7 +59,7 @@"],
+ [62, 59, " },"],
+ [63, 60, " {"],
+ [64, 61, " \"name\": \"gnome-desktop\","],
+ [65, 62, "- \"config-opts\": [\"--disable-debug-tools\", \"--disable-udev\"],"],
+ [66, 62, "+ \"config-opts\": [\"--disable-debug-tools\", \"--disable-\"],"],
+ [66, 63, " \"sources\": ["],
+ [67, 64, " {"],
+ [68, 65, " \"type\": \"git\","],
+ [83, 80, "@@ -83,11 +80,6 @@"],
+ [83, 80, " \"buildsystem\": \"meson\","],
+ [84, 81, " \"builddir\": true,"],
+ [85, 82, " \"name\": \"nautilus\","],
+ [86, 83, "- \"config-opts\": ["],
+ [87, 83, "- \"-Denable-desktop=false\","],
+ [88, 83, "- \"-Denable-selinux=false\","],
+ [89, 83, "- \"--libdir=/app/lib\""],
+ [90, 83, "- ],"],
+ [91, 83, " \"sources\": ["],
+ [92, 84, " {"],
+ [93, 85, " \"type\": \"git\","]]
+ end
+
+ it 'return merge of blob lines with diff lines correctly' do
+ new_diff_lines = subject.unfolded_diff_lines
+
+ expected_diff_lines.each_with_index do |expected_line, i|
+ line = new_diff_lines[i]
+
+ expect([line.old_pos, line.new_pos, line.text])
+ .to eq([expected_line[0], expected_line[1], expected_line[2]])
+ end
+ end
+
+ it 'merged lines have correct line codes' do
+ new_diff_lines = subject.unfolded_diff_lines
+
+ new_diff_lines.each_with_index do |line, i|
+ old_pos, new_pos = expected_diff_lines[i][0], expected_diff_lines[i][1]
+
+ unless line.type == 'match'
+ expect(line.line_code).to eq(Gitlab::Git.diff_line_code(diff_file.file_path, new_pos, old_pos))
+ end
+ end
+ end
+ end
+ end
+
+ context 'position sits between two match lines (no expasion needed)' do
+ let(:position) do
+ Gitlab::Diff::Position.new(base_sha: "1c59dfa64afbea8c721bb09a06a9d326c952ea19",
+ start_sha: "1c59dfa64afbea8c721bb09a06a9d326c952ea19",
+ head_sha: "1487062132228de836236c522fe52fed4980a46c",
+ old_path: "build-aux/flatpak/org.gnome.Nautilus.json",
+ new_path: "build-aux/flatpak/org.gnome.Nautilus.json",
+ position_type: "text",
+ old_line: 64,
+ new_line: 61)
+ end
+
+ context 'diff lines' do
+ it 'returns nil' do
+ expect(subject.unfolded_diff_lines).to be_nil
+ end
+ end
+ end
+
+ context 'position requires bottom expansion and new match lines' do
+ let(:position) do
+ Gitlab::Diff::Position.new(base_sha: "1c59dfa64afbea8c721bb09a06a9d326c952ea19",
+ start_sha: "1c59dfa64afbea8c721bb09a06a9d326c952ea19",
+ head_sha: "1487062132228de836236c522fe52fed4980a46c",
+ old_path: "build-aux/flatpak/org.gnome.Nautilus.json",
+ new_path: "build-aux/flatpak/org.gnome.Nautilus.json",
+ position_type: "text",
+ old_line: 107,
+ new_line: 99)
+ end
+
+ context 'blob lines' do
+ let(:expected_blob_lines) do
+ [[104, 104, " \"sdk\": \"foo\","],
+ [105, 105, " \"command\": \"foo\","],
+ [106, 106, " \"tags\": [\"foo\", \"bar\", \"kux\"],"],
+ [107, 107, " \"desktop-file-name-prefix\": \"(Foo) \","],
+ [108, 108, " {"],
+ [109, 109, " \"buildsystem\": \"meson\","],
+ [110, 110, " \"builddir\": true,"]]
+ end
+
+ it 'returns the extracted blob lines correctly' do
+ extracted_lines = subject.blob_lines
+
+ expect(extracted_lines.size).to eq(7)
+
+ extracted_lines.each_with_index do |line, i|
+ expect([line.old_line, line.new_line, line.text]).to eq(expected_blob_lines[i])
+ end
+ end
+ end
+
+ context 'diff lines' do
+ let(:expected_diff_lines) do
+ [[7, 7, "@@ -7,9 +7,6 @@"],
+ [7, 7, " \"tags\": [\"devel\", \"development\", \"nightly\"],"],
+ [8, 8, " \"desktop-file-name-prefix\": \"(Development) \","],
+ [9, 9, " \"finish-args\": ["],
+ [10, 10, "- \"--share=ipc\", \"--socket=x11\","],
+ [11, 10, "- \"--socket=wayland\","],
+ [12, 10, "- \"--talk-name=org.gnome.OnlineAccounts\","],
+ [13, 10, " \"--talk-name=org.freedesktop.Tracker1\","],
+ [14, 11, " \"--filesystem=home\","],
+ [15, 12, " \"--talk-name=org.gtk.vfs\", \"--talk-name=org.gtk.vfs.*\","],
+ [62, 59, "@@ -62,7 +59,7 @@"],
+ [62, 59, " },"],
+ [63, 60, " {"],
+ [64, 61, " \"name\": \"gnome-desktop\","],
+ [65, 62, "- \"config-opts\": [\"--disable-debug-tools\", \"--disable-udev\"],"],
+ [66, 62, "+ \"config-opts\": [\"--disable-debug-tools\", \"--disable-\"],"],
+ [66, 63, " \"sources\": ["],
+ [67, 64, " {"],
+ [68, 65, " \"type\": \"git\","],
+ [83, 80, "@@ -83,11 +80,6 @@"],
+ [83, 80, " \"buildsystem\": \"meson\","],
+ [84, 81, " \"builddir\": true,"],
+ [85, 82, " \"name\": \"nautilus\","],
+ [86, 83, "- \"config-opts\": ["],
+ [87, 83, "- \"-Denable-desktop=false\","],
+ [88, 83, "- \"-Denable-selinux=false\","],
+ [89, 83, "- \"--libdir=/app/lib\""],
+ [90, 83, "- ],"],
+ [91, 83, " \"sources\": ["],
+ [92, 84, " {"],
+ [93, 85, " \"type\": \"git\","],
+ # New match line
+ [104, 96, "@@ -104,7+96,7 @@"],
+
+ # Injected blob lines
+ [104, 96, " \"sdk\": \"foo\","],
+ [105, 97, " \"command\": \"foo\","],
+ [106, 98, " \"tags\": [\"foo\", \"bar\", \"kux\"],"],
+ [107, 99, " \"desktop-file-name-prefix\": \"(Foo) \","],
+ [108, 100, " {"],
+ [109, 101, " \"buildsystem\": \"meson\","],
+ [110, 102, " \"builddir\": true,"]]
+ # end
+ end
+
+ it 'return merge of blob lines with diff lines correctly' do
+ new_diff_lines = subject.unfolded_diff_lines
+
+ expected_diff_lines.each_with_index do |expected_line, i|
+ line = new_diff_lines[i]
+
+ expect([line.old_pos, line.new_pos, line.text])
+ .to eq([expected_line[0], expected_line[1], expected_line[2]])
+ end
+ end
+
+ it 'merged lines have correct line codes' do
+ new_diff_lines = subject.unfolded_diff_lines
+
+ new_diff_lines.each_with_index do |line, i|
+ old_pos, new_pos = expected_diff_lines[i][0], expected_diff_lines[i][1]
+
+ unless line.type == 'match'
+ expect(line.line_code).to eq(Gitlab::Git.diff_line_code(diff_file.file_path, new_pos, old_pos))
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb
index 2d94356f386..cc4faf6f10b 100644
--- a/spec/lib/gitlab/diff/position_spec.rb
+++ b/spec/lib/gitlab/diff/position_spec.rb
@@ -566,13 +566,13 @@ describe Gitlab::Diff::Position do
end
end
- context "for text positon" do
+ context "for text position" do
let(:args) { args_for_text }
it_behaves_like "diff position json"
end
- context "for image positon" do
+ context "for image position" do
let(:args) { args_for_img }
it_behaves_like "diff position json"
@@ -592,13 +592,13 @@ describe Gitlab::Diff::Position do
end
end
- context "for text positon" do
+ context "for text position" do
let(:args) { args_for_text }
it_behaves_like "diff position json"
end
- context "for image positon" do
+ context "for image position" do
let(:args) { args_for_img }
it_behaves_like "diff position json"
diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb
index ddc4f6c5b5c..a2eed07ca55 100644
--- a/spec/lib/gitlab/diff/position_tracer_spec.rb
+++ b/spec/lib/gitlab/diff/position_tracer_spec.rb
@@ -43,7 +43,7 @@ describe Gitlab::Diff::PositionTracer do
#
# In any case, all of this means that the tests below will be extremely
# (excessively, unjustifiably) thorough for scenarios where "the file was
- # created in the old diff" and then drop off to comparitively lackluster
+ # created in the old diff" and then drop off to comparatively lackluster
# testing of other scenarios.
#
# I did still try to cover most of the obvious and potentially tricky
diff --git a/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb b/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb
index ace3104f36f..f276f1a8ddf 100644
--- a/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb
@@ -93,5 +93,74 @@ describe Gitlab::Email::Handler::CreateMergeRequestHandler do
end
end
end
+
+ context 'when the email contains patch attachments' do
+ let(:email_raw) { fixture_file("emails/valid_merge_request_with_patch.eml") }
+
+ it 'creates the source branch and applies the patches' do
+ receiver.execute
+
+ branch = project.repository.find_branch('new-branch-with-a-patch')
+
+ expect(branch).not_to be_nil
+ expect(branch.dereferenced_target.message).to include('A commit from a patch')
+ end
+
+ it 'creates the merge request' do
+ expect { receiver.execute }
+ .to change { project.merge_requests.where(source_branch: 'new-branch-with-a-patch').size }.by(1)
+ end
+
+ it 'does not mention the patches in the created merge request' do
+ receiver.execute
+
+ merge_request = project.merge_requests.find_by!(source_branch: 'new-branch-with-a-patch')
+
+ expect(merge_request.description).not_to include('0001-A-commit-from-a-patch.patch')
+ end
+
+ context 'when the patch could not be applied' do
+ let(:email_raw) { fixture_file("emails/merge_request_with_conflicting_patch.eml") }
+
+ it 'raises an error' do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidAttachment)
+ end
+ end
+
+ context 'when specifying the target branch using quick actions' do
+ let(:email_raw) { fixture_file('emails/merge_request_with_patch_and_target_branch.eml') }
+
+ it 'creates the merge request with the correct target branch' do
+ receiver.execute
+
+ merge_request = project.merge_requests.find_by!(source_branch: 'new-branch-with-a-patch')
+
+ expect(merge_request.target_branch).to eq('with-codeowners')
+ end
+
+ it 'based the merge request of the target_branch' do
+ receiver.execute
+
+ merge_request = project.merge_requests.find_by!(source_branch: 'new-branch-with-a-patch')
+
+ expect(merge_request.diff_base_commit).to eq(project.repository.commit('with-codeowners'))
+ end
+ end
+ end
+ end
+
+ describe '#patch_attachments' do
+ let(:email_raw) { fixture_file('emails/merge_request_multiple_patches.eml') }
+ let(:mail) { Mail::Message.new(email_raw) }
+ subject(:handler) { described_class.new(mail, mail_key) }
+
+ it 'orders attachments ending in `.patch` by name' do
+ expected_filenames = ["0001-A-commit-from-a-patch.patch",
+ "0002-This-does-not-apply-to-the-feature-branch.patch"]
+
+ attachments = handler.__send__(:patch_attachments).map(&:filename)
+
+ expect(attachments).to eq(expected_filenames)
+ end
end
end
diff --git a/spec/lib/gitlab/file_detector_spec.rb b/spec/lib/gitlab/file_detector_spec.rb
index 294ec2c2fd6..edab53247e9 100644
--- a/spec/lib/gitlab/file_detector_spec.rb
+++ b/spec/lib/gitlab/file_detector_spec.rb
@@ -15,7 +15,12 @@ describe Gitlab::FileDetector do
describe '.type_of' do
it 'returns the type of a README file' do
- expect(described_class.type_of('README.md')).to eq(:readme)
+ %w[README readme INDEX index].each do |filename|
+ expect(described_class.type_of(filename)).to eq(:readme)
+ %w[.md .adoc .rst].each do |extname|
+ expect(described_class.type_of(filename + extname)).to eq(:readme)
+ end
+ end
end
it 'returns nil for a README file in a directory' do
diff --git a/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb b/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb
index ca067a29174..134bd5657e7 100644
--- a/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb
+++ b/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb
@@ -17,7 +17,7 @@ describe Gitlab::Git::AttributesAtRefParser, :seed_helper do
end
it 'handles missing blobs' do
- expect { described_class.new(repository, 'non-existant-branch') }.not_to raise_error
+ expect { described_class.new(repository, 'non-existent-branch') }.not_to raise_error
end
describe '#attributes' do
diff --git a/spec/lib/gitlab/git/attributes_parser_spec.rb b/spec/lib/gitlab/git/attributes_parser_spec.rb
index 18ebfef38f0..f431d4e2a53 100644
--- a/spec/lib/gitlab/git/attributes_parser_spec.rb
+++ b/spec/lib/gitlab/git/attributes_parser_spec.rb
@@ -94,7 +94,7 @@ describe Gitlab::Git::AttributesParser, :seed_helper do
# It's a bit hard to test for something _not_ being processed. As such we'll
# just test the number of entries.
it 'ignores any comments and empty lines' do
- expect(subject.patterns.length).to eq(10)
+ expect(subject.patterns.length).to eq(12)
end
end
@@ -126,7 +126,7 @@ describe Gitlab::Git::AttributesParser, :seed_helper do
describe '#each_line' do
it 'iterates over every line in the attributes file' do
- args = [String] * 14 # the number of lines in the file
+ args = [String] * 16 # the number of lines in the file
expect { |b| subject.each_line(&b) }.to yield_successive_args(*args)
end
diff --git a/spec/lib/gitlab/git/patches/collection_spec.rb b/spec/lib/gitlab/git/patches/collection_spec.rb
new file mode 100644
index 00000000000..080be141c59
--- /dev/null
+++ b/spec/lib/gitlab/git/patches/collection_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::Git::Patches::Collection do
+ let(:patches_folder) { Rails.root.join('spec/fixtures/patchfiles') }
+ let(:patch_content1) do
+ File.read(File.join(patches_folder, "0001-This-does-not-apply-to-the-feature-branch.patch"))
+ end
+ let(:patch_content2) do
+ File.read(File.join(patches_folder, "0001-A-commit-from-a-patch.patch"))
+ end
+
+ subject(:collection) { described_class.new([patch_content1, patch_content2]) }
+
+ describe '#size' do
+ it 'combines the size of the patches' do
+ expect(collection.size).to eq(549.bytes + 424.bytes)
+ end
+ end
+
+ describe '#valid_size?' do
+ it 'is not valid if the total size is bigger than 2MB' do
+ expect(collection).to receive(:size).and_return(2500.kilobytes)
+
+ expect(collection).not_to be_valid_size
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/patches/commit_patches_spec.rb b/spec/lib/gitlab/git/patches/commit_patches_spec.rb
new file mode 100644
index 00000000000..760112155ce
--- /dev/null
+++ b/spec/lib/gitlab/git/patches/commit_patches_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::Git::Patches::CommitPatches do
+ describe '#commit' do
+ let(:patches) do
+ patches_folder = Rails.root.join('spec/fixtures/patchfiles')
+ content_1 = File.read(File.join(patches_folder, "0001-This-does-not-apply-to-the-feature-branch.patch"))
+ content_2 = File.read(File.join(patches_folder, "0001-A-commit-from-a-patch.patch"))
+
+ Gitlab::Git::Patches::Collection.new([content_1, content_2])
+ end
+ let(:user) { build(:user) }
+ let(:branch_name) { 'branch-with-patches' }
+ let(:repository) { create(:project, :repository).repository }
+
+ subject(:commit_patches) do
+ described_class.new(user, repository, branch_name, patches)
+ end
+
+ it 'applies the patches' do
+ new_rev = commit_patches.commit
+
+ expect(repository.commit(new_rev)).not_to be_nil
+ end
+
+ it 'updates the branch cache' do
+ expect(repository).to receive(:after_create_branch)
+
+ commit_patches.commit
+ end
+
+ context 'when the repository does not exist' do
+ let(:repository) { create(:project).repository }
+
+ it 'raises the correct error' do
+ expect { commit_patches.commit }.to raise_error(Gitlab::Git::Repository::NoRepository)
+ end
+ end
+
+ context 'when the patch does not apply' do
+ let(:branch_name) { 'feature' }
+
+ it 'raises the correct error' do
+ expect { commit_patches.commit }.to raise_error(Gitlab::Git::CommandError)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/patches/patch_spec.rb b/spec/lib/gitlab/git/patches/patch_spec.rb
new file mode 100644
index 00000000000..7466e853b65
--- /dev/null
+++ b/spec/lib/gitlab/git/patches/patch_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::Git::Patches::Patch do
+ let(:patches_folder) { Rails.root.join('spec/fixtures/patchfiles') }
+ let(:patch_content) do
+ File.read(File.join(patches_folder, "0001-This-does-not-apply-to-the-feature-branch.patch"))
+ end
+ let(:patch) { described_class.new(patch_content) }
+
+ describe '#size' do
+ it 'is correct' do
+ expect(patch.size).to eq(549.bytes)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 51eb997a325..1fe73c12fc0 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -476,6 +476,27 @@ describe Gitlab::Git::Repository, :seed_helper do
end
end
+ describe '#fetch_remote' do
+ it 'delegates to the gitaly RepositoryService' do
+ ssh_auth = double(:ssh_auth)
+ expected_opts = {
+ ssh_auth: ssh_auth,
+ forced: true,
+ no_tags: true,
+ timeout: described_class::GITLAB_PROJECTS_TIMEOUT,
+ prune: false
+ }
+
+ expect(repository.gitaly_repository_client).to receive(:fetch_remote).with('remote-name', expected_opts)
+
+ repository.fetch_remote('remote-name', ssh_auth: ssh_auth, forced: true, no_tags: true, prune: false)
+ end
+
+ it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RepositoryService, :fetch_remote do
+ subject { repository.fetch_remote('remote-name') }
+ end
+ end
+
describe '#find_remote_root_ref' do
it 'gets the remote root ref from GitalyClient' do
expect_any_instance_of(Gitlab::GitalyClient::RemoteService)
@@ -1074,12 +1095,26 @@ describe Gitlab::Git::Repository, :seed_helper do
end
it 'returns no Gitaly::DiffStats when there is a nil SHA' do
+ expect_any_instance_of(Gitlab::GitalyClient::CommitService)
+ .not_to receive(:diff_stats)
+
collection = repository.diff_stats(nil, 'master')
expect(collection).to be_a(Gitlab::Git::DiffStatsCollection)
expect(collection).to be_a(Enumerable)
expect(collection.to_a).to be_empty
end
+
+ it 'returns no Gitaly::DiffStats when there is a BLANK_SHA' do
+ expect_any_instance_of(Gitlab::GitalyClient::CommitService)
+ .not_to receive(:diff_stats)
+
+ collection = repository.diff_stats(Gitlab::Git::BLANK_SHA, 'master')
+
+ expect(collection).to be_a(Gitlab::Git::DiffStatsCollection)
+ expect(collection).to be_a(Enumerable)
+ expect(collection.to_a).to be_empty
+ end
end
describe "#ls_files" do
@@ -1194,6 +1229,34 @@ describe Gitlab::Git::Repository, :seed_helper do
end
end
+ describe '#gitattribute' do
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_GITATTRIBUTES_REPO_PATH, '') }
+
+ after do
+ ensure_seeds
+ end
+
+ it 'returns matching language attribute' do
+ expect(repository.gitattribute("custom-highlighting/test.gitlab-custom", 'gitlab-language')).to eq('ruby')
+ end
+
+ it 'returns matching language attribute with additional options' do
+ expect(repository.gitattribute("custom-highlighting/test.gitlab-cgi", 'gitlab-language')).to eq('erb?parent=json')
+ end
+
+ it 'returns nil if nothing matches' do
+ expect(repository.gitattribute("report.xslt", 'gitlab-language')).to eq(nil)
+ end
+
+ context 'without gitattributes file' do
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
+
+ it 'returns nil' do
+ expect(repository.gitattribute("README.md", 'gitlab-language')).to eq(nil)
+ end
+ end
+ end
+
describe '#ref_exists?' do
it 'returns true for an existing tag' do
expect(repository.ref_exists?('refs/heads/master')).to eq(true)
diff --git a/spec/lib/gitlab/git/wraps_gitaly_errors_spec.rb b/spec/lib/gitlab/git/wraps_gitaly_errors_spec.rb
new file mode 100644
index 00000000000..bcf4814edb6
--- /dev/null
+++ b/spec/lib/gitlab/git/wraps_gitaly_errors_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe Gitlab::Git::WrapsGitalyErrors do
+ subject(:wrapper) do
+ klazz = Class.new { include Gitlab::Git::WrapsGitalyErrors }
+ klazz.new
+ end
+
+ describe "#wrapped_gitaly_errors" do
+ mapping = {
+ GRPC::NotFound => Gitlab::Git::Repository::NoRepository,
+ GRPC::InvalidArgument => ArgumentError,
+ GRPC::BadStatus => Gitlab::Git::CommandError
+ }
+
+ mapping.each do |grpc_error, error|
+ it "wraps #{grpc_error} in a #{error}" do
+ expect { wrapper.wrapped_gitaly_errors { raise grpc_error.new('wrapped') } }
+ .to raise_error(error)
+ end
+ end
+
+ it 'does not swallow other errors' do
+ expect { wrapper.wrapped_gitaly_errors { raise 'raised' } }
+ .to raise_error(RuntimeError)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
index eaf64e3c9b4..b37fe2686b6 100644
--- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
@@ -335,4 +335,37 @@ describe Gitlab::GitalyClient::OperationService do
end
end
end
+
+ describe '#user_commit_patches' do
+ let(:patches_folder) { Rails.root.join('spec/fixtures/patchfiles') }
+ let(:patch_content) do
+ patch_names.map { |name| File.read(File.join(patches_folder, name)) }.join("\n")
+ end
+ let(:patch_names) { %w(0001-This-does-not-apply-to-the-feature-branch.patch) }
+ let(:branch_name) { 'branch-with-patches' }
+
+ subject(:commit_patches) do
+ client.user_commit_patches(user, branch_name, patch_content)
+ end
+
+ it 'applies the patch correctly' do
+ branch_update = commit_patches
+
+ expect(branch_update).to be_branch_created
+
+ commit = repository.commit(branch_update.newrev)
+ expect(commit.author_email).to eq('patchuser@gitlab.org')
+ expect(commit.committer_email).to eq(user.email)
+ expect(commit.message.chomp).to eq('This does not apply to the `feature` branch')
+ end
+
+ context 'when the patch could not be applied' do
+ let(:patch_names) { %w(0001-This-does-not-apply-to-the-feature-branch.patch) }
+ let(:branch_name) { 'feature' }
+
+ it 'raises the correct error' do
+ expect { commit_patches }.to raise_error(GRPC::FailedPrecondition)
+ end
+ 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 1547d447197..d605fcbafee 100644
--- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Gitlab::GitalyClient::RepositoryService do
+ using RSpec::Parameterized::TableSyntax
+
let(:project) { create(:project) }
let(:storage_name) { project.repository_storage }
let(:relative_path) { project.disk_path + '.git' }
@@ -107,16 +109,67 @@ describe Gitlab::GitalyClient::RepositoryService do
end
describe '#fetch_remote' do
- let(:ssh_auth) { double(:ssh_auth, ssh_import?: true, ssh_key_auth?: false, ssh_known_hosts: nil) }
- let(:import_url) { 'ssh://example.com' }
+ let(:remote) { 'remote-name' }
it 'sends a fetch_remote_request message' do
+ expected_request = gitaly_request_with_params(
+ remote: remote,
+ ssh_key: '',
+ known_hosts: '',
+ force: false,
+ no_tags: false,
+ no_prune: false
+ )
+
expect_any_instance_of(Gitaly::RepositoryService::Stub)
.to receive(:fetch_remote)
- .with(gitaly_request_with_params(no_prune: false), kind_of(Hash))
+ .with(expected_request, kind_of(Hash))
.and_return(double(value: true))
- client.fetch_remote(import_url, ssh_auth: ssh_auth, forced: false, no_tags: false, timeout: 60)
+ client.fetch_remote(remote, ssh_auth: nil, forced: false, no_tags: false, timeout: 1)
+ end
+
+ context 'SSH auth' do
+ where(:ssh_import, :ssh_key_auth, :ssh_private_key, :ssh_known_hosts, :expected_params) do
+ false | false | 'key' | 'known_hosts' | {}
+ false | true | 'key' | 'known_hosts' | {}
+ true | false | 'key' | 'known_hosts' | { known_hosts: 'known_hosts' }
+ true | true | 'key' | 'known_hosts' | { ssh_key: 'key', known_hosts: 'known_hosts' }
+ true | true | 'key' | nil | { ssh_key: 'key' }
+ true | true | nil | 'known_hosts' | { known_hosts: 'known_hosts' }
+ true | true | nil | nil | {}
+ true | true | '' | '' | {}
+ end
+
+ with_them do
+ let(:ssh_auth) do
+ double(
+ :ssh_auth,
+ ssh_import?: ssh_import,
+ ssh_key_auth?: ssh_key_auth,
+ ssh_private_key: ssh_private_key,
+ ssh_known_hosts: ssh_known_hosts
+ )
+ end
+
+ it do
+ expected_request = gitaly_request_with_params({
+ remote: remote,
+ ssh_key: '',
+ known_hosts: '',
+ force: false,
+ no_tags: false,
+ no_prune: false
+ }.update(expected_params))
+
+ expect_any_instance_of(Gitaly::RepositoryService::Stub)
+ .to receive(:fetch_remote)
+ .with(expected_request, kind_of(Hash))
+ .and_return(double(value: true))
+
+ client.fetch_remote(remote, ssh_auth: ssh_auth, forced: false, no_tags: false, timeout: 1)
+ end
+ end
end
end
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 25684ea9e2c..efca8564894 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
@@ -240,7 +240,12 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi
.and_return(user.id)
end
- it 'returns the existing merge request' do
+ # TODO: remove rails5-only after removing rails4 tests
+ # rails 4 can not handle multiple indexes on the same column set if
+ # index was added by 't.index' - t.index is used by default in schema.rb in
+ # rails 5. Let's run this test only in rails 5 env:
+ # see https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21492#note_113602758
+ it 'returns the existing merge request', :rails5 do
mr1, exists1 = importer.create_merge_request
mr2, exists2 = importer.create_merge_request
diff --git a/spec/lib/gitlab/gpg_spec.rb b/spec/lib/gitlab/gpg_spec.rb
index 47f37cae98f..39d09c49989 100644
--- a/spec/lib/gitlab/gpg_spec.rb
+++ b/spec/lib/gitlab/gpg_spec.rb
@@ -96,7 +96,7 @@ describe Gitlab::Gpg do
expect(described_class.current_home_dir).to eq default_home_dir
end
- it 'returns the explicitely set home dir' do
+ it 'returns the explicitly set home dir' do
GPGME::Engine.home_dir = '/tmp/gpg'
expect(described_class.current_home_dir).to eq '/tmp/gpg'
@@ -104,7 +104,7 @@ describe Gitlab::Gpg do
GPGME::Engine.home_dir = GPGME::Engine.dirinfo('homedir')
end
- it 'returns the default value when explicitely setting the home dir to nil' do
+ it 'returns the default value when explicitly setting the home dir to nil' do
GPGME::Engine.home_dir = nil
expect(described_class.current_home_dir).to eq default_home_dir
diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb
index 88f7099ff3c..fe0e9702f8a 100644
--- a/spec/lib/gitlab/highlight_spec.rb
+++ b/spec/lib/gitlab/highlight_spec.rb
@@ -5,44 +5,85 @@ describe Gitlab::Highlight do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
- let(:commit) { project.commit(sample_commit.id) }
-
- describe 'custom highlighting from .gitattributes' do
- let(:branch) { 'gitattributes' }
- let(:blob) { repository.blob_at_branch(branch, path) }
+ describe 'language provided' do
let(:highlighter) do
- described_class.new(blob.path, blob.data, repository: repository)
+ described_class.new('foo.erb', 'bar', language: 'erb?parent=json')
end
- before do
- project.change_head('gitattributes')
+ it 'sets correct lexer' do
+ expect(highlighter.lexer.tag).to eq 'erb'
+ expect(highlighter.lexer.parent.tag).to eq 'json'
end
+ end
- describe 'basic language selection' do
- let(:path) { 'custom-highlighting/test.gitlab-custom' }
- it 'highlights as ruby' do
- expect(highlighter.lexer.tag).to eq 'ruby'
- end
+ describe '#highlight' do
+ let(:file_name) { 'test.lisp' }
+ let(:no_context_content) { ":type \"assem\"))" }
+ let(:content) { "(make-pathname :defaults name\n#{no_context_content}" }
+ let(:multiline_content) do
+ %q(
+ def test(input):
+ """This is line 1 of a multi-line comment.
+ This is line 2.
+ """
+ )
+ end
+
+ it 'highlights' do
+ expected = %Q[<span id="LC1" class="line" lang="common_lisp"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span>
+<span id="LC2" class="line" lang="common_lisp"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span>]
+
+ expect(described_class.highlight(file_name, content)).to eq(expected)
+ end
+
+ it 'returns plain version for unknown lexer context' do
+ result = described_class.highlight(file_name, no_context_content)
+
+ expect(result).to eq(%[<span id="LC1" class="line" lang="">:type "assem"))</span>])
end
- describe 'cgi options' do
- let(:path) { 'custom-highlighting/test.gitlab-cgi' }
+ it 'returns plain version for long content' do
+ stub_const('Gitlab::Highlight::MAXIMUM_TEXT_HIGHLIGHT_SIZE', 1)
+ result = described_class.highlight(file_name, content)
- it 'highlights as json with erb' do
- expect(highlighter.lexer.tag).to eq 'erb'
- expect(highlighter.lexer.parent.tag).to eq 'json'
+ expect(result).to eq(%[<span id="LC1" class="line" lang="">(make-pathname :defaults name</span>\n<span id="LC2" class="line" lang="">:type "assem"))</span>])
+ end
+
+ it 'highlights multi-line comments' do
+ result = described_class.highlight(file_name, multiline_content)
+ html = Nokogiri::HTML(result)
+ lines = html.search('.s')
+
+ expect(lines.count).to eq(3)
+ expect(lines[0].text).to eq('"""This is line 1 of a multi-line comment.')
+ expect(lines[1].text).to eq(' This is line 2.')
+ expect(lines[2].text).to eq(' """')
+ end
+
+ context 'diff highlighting' do
+ let(:file_name) { 'test.diff' }
+ let(:content) { "+aaa\n+bbb\n- ccc\n ddd\n"}
+ let(:expected) do
+ %q(<span id="LC1" class="line" lang="diff"><span class="gi">+aaa</span></span>
+<span id="LC2" class="line" lang="diff"><span class="gi">+bbb</span></span>
+<span id="LC3" class="line" lang="diff"><span class="gd">- ccc</span></span>
+<span id="LC4" class="line" lang="diff"> ddd</span>)
+ end
+
+ it 'highlights each line properly' do
+ result = described_class.highlight(file_name, content)
+
+ expect(result).to eq(expected)
end
end
- end
- describe '#highlight' do
describe 'with CRLF' do
let(:branch) { 'crlf-diff' }
let(:path) { 'files/whitespace' }
let(:blob) { repository.blob_at_branch(branch, path) }
let(:lines) do
- described_class.highlight(blob.path, blob.data, repository: repository).lines
+ described_class.highlight(blob.path, blob.data).lines
end
it 'strips extra LFs' do
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index a63f34b5536..1d184375a52 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -198,6 +198,7 @@ project:
- last_event
- services
- campfire_service
+- discord_service
- drone_ci_service
- emails_on_push_service
- pipelines_email_service
@@ -299,6 +300,7 @@ project:
- ci_cd_settings
- import_export_upload
- repository_languages
+- pool_repository
award_emoji:
- awardable
- user
diff --git a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
index 65f073b2df3..87ab81d8169 100644
--- a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
+++ b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
@@ -23,15 +23,23 @@ describe 'Import/Export attribute configuration' do
let(:safe_attributes_file) { 'spec/lib/gitlab/import_export/safe_model_attributes.yml' }
let(:safe_model_attributes) { YAML.load_file(safe_attributes_file) }
+ let(:ee_safe_attributes_file) { 'ee/spec/lib/gitlab/import_export/safe_model_attributes.yml' }
+ let(:ee_safe_model_attributes) { File.exist?(ee_safe_attributes_file) ? YAML.load_file(ee_safe_attributes_file) : {} }
+
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
- expect(safe_model_attributes[relation_class.to_s]).not_to be_nil, "Expected exported class #{relation_class} to exist in safe_model_attributes"
-
current_attributes = parsed_attributes(relation_name, relation_attributes)
- safe_attributes = safe_model_attributes[relation_class.to_s]
+ safe_attributes = safe_model_attributes[relation_class.to_s].dup || []
+
+ ee_safe_model_attributes[relation_class.to_s].to_a.each do |attribute|
+ safe_attributes << attribute
+ end
+
+ expect(safe_attributes).not_to be_nil, "Expected exported class #{relation_class} to exist in safe_model_attributes"
+
new_attributes = current_attributes - safe_attributes
expect(new_attributes).to be_empty, failure_message(relation_class.to_s, new_attributes)
@@ -43,6 +51,7 @@ describe 'Import/Export attribute configuration' do
It looks like #{relation_class}, which is exported using the project Import/Export, has new attributes: #{new_attributes.join(',')}
Please add the attribute(s) to SAFE_MODEL_ATTRIBUTES if you consider this can be exported.
+ #{"If the model/associations are EE-specific, use `#{File.expand_path(ee_safe_attributes_file)}`.\n" if ee_safe_model_attributes.any?}
Otherwise, please blacklist the attribute(s) in IMPORT_EXPORT_CONFIG by adding it to its correspondent
model in the +excluded_attributes+ section.
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index b0570680d5a..365bfae0d88 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -321,7 +321,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
end
- context 'when the project has overriden params in import data' do
+ context 'when the project has overridden params in import data' do
it 'overwrites the params stored in the JSON' do
project.create_import_data(data: { override_params: { description: "Overridden" } })
diff --git a/spec/lib/gitlab/kubernetes/helm/api_spec.rb b/spec/lib/gitlab/kubernetes/helm/api_spec.rb
index 9200724ed23..8bce7a4cdf5 100644
--- a/spec/lib/gitlab/kubernetes/helm/api_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/api_spec.rb
@@ -36,6 +36,7 @@ describe Gitlab::Kubernetes::Helm::Api do
describe '#install' do
before do
allow(client).to receive(:create_pod).and_return(nil)
+ allow(client).to receive(:get_config_map).and_return(nil)
allow(client).to receive(:create_config_map).and_return(nil)
allow(client).to receive(:create_service_account).and_return(nil)
allow(client).to receive(:create_cluster_role_binding).and_return(nil)
@@ -57,6 +58,18 @@ describe Gitlab::Kubernetes::Helm::Api do
subject.install(command)
end
+
+ context 'config map already exists' do
+ before do
+ expect(client).to receive(:get_config_map).with("values-content-configuration-#{application_name}", gitlab_namespace).and_return(resource)
+ end
+
+ it 'updates the config map' do
+ expect(client).to receive(:update_config_map).with(resource).once
+
+ subject.install(command)
+ end
+ end
end
context 'without a service account' do
@@ -88,8 +101,8 @@ describe Gitlab::Kubernetes::Helm::Api do
context 'service account and cluster role binding does not exist' do
before do
- expect(client).to receive('get_service_account').with('tiller', 'gitlab-managed-apps').and_raise(Kubeclient::HttpError.new(404, 'Not found', nil))
- expect(client).to receive('get_cluster_role_binding').with('tiller-admin').and_raise(Kubeclient::HttpError.new(404, 'Not found', nil))
+ expect(client).to receive(:get_service_account).with('tiller', 'gitlab-managed-apps').and_raise(Kubeclient::ResourceNotFoundError.new(404, 'Not found', nil))
+ expect(client).to receive(:get_cluster_role_binding).with('tiller-admin').and_raise(Kubeclient::ResourceNotFoundError.new(404, 'Not found', nil))
end
it 'creates a service account, followed the cluster role binding on kubeclient' do
@@ -102,8 +115,8 @@ describe Gitlab::Kubernetes::Helm::Api do
context 'service account already exists' do
before do
- expect(client).to receive('get_service_account').with('tiller', 'gitlab-managed-apps').and_return(service_account_resource)
- expect(client).to receive('get_cluster_role_binding').with('tiller-admin').and_raise(Kubeclient::HttpError.new(404, 'Not found', nil))
+ expect(client).to receive(:get_service_account).with('tiller', 'gitlab-managed-apps').and_return(service_account_resource)
+ expect(client).to receive(:get_cluster_role_binding).with('tiller-admin').and_raise(Kubeclient::ResourceNotFoundError.new(404, 'Not found', nil))
end
it 'updates the service account, followed by creating the cluster role binding' do
@@ -116,8 +129,8 @@ describe Gitlab::Kubernetes::Helm::Api do
context 'service account and cluster role binding already exists' do
before do
- expect(client).to receive('get_service_account').with('tiller', 'gitlab-managed-apps').and_return(service_account_resource)
- expect(client).to receive('get_cluster_role_binding').with('tiller-admin').and_return(cluster_role_binding_resource)
+ expect(client).to receive(:get_service_account).with('tiller', 'gitlab-managed-apps').and_return(service_account_resource)
+ expect(client).to receive(:get_cluster_role_binding).with('tiller-admin').and_return(cluster_role_binding_resource)
end
it 'updates the service account, followed by creating the cluster role binding' do
@@ -130,7 +143,7 @@ describe Gitlab::Kubernetes::Helm::Api do
context 'a non-404 error is thrown' do
before do
- expect(client).to receive('get_service_account').with('tiller', 'gitlab-managed-apps').and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil))
+ expect(client).to receive(:get_service_account).with('tiller', 'gitlab-managed-apps').and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil))
end
it 'raises an error' do
diff --git a/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb
index 72dc1817936..4a3b9d4bf6a 100644
--- a/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb
@@ -8,7 +8,7 @@ describe Gitlab::Kubernetes::Helm::InitCommand do
let(:commands) do
<<~EOS
- helm init --tiller-tls --tiller-tls-verify --tls-ca-cert /data/helm/helm/config/ca.pem --tiller-tls-cert /data/helm/helm/config/cert.pem --tiller-tls-key /data/helm/helm/config/key.pem >/dev/null
+ helm init --tiller-tls --tiller-tls-verify --tls-ca-cert /data/helm/helm/config/ca.pem --tiller-tls-cert /data/helm/helm/config/cert.pem --tiller-tls-key /data/helm/helm/config/key.pem
EOS
end
@@ -22,7 +22,7 @@ describe Gitlab::Kubernetes::Helm::InitCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --tiller-tls --tiller-tls-verify --tls-ca-cert /data/helm/helm/config/ca.pem --tiller-tls-cert /data/helm/helm/config/cert.pem --tiller-tls-key /data/helm/helm/config/key.pem --service-account tiller >/dev/null
+ helm init --tiller-tls --tiller-tls-verify --tls-ca-cert /data/helm/helm/config/ca.pem --tiller-tls-cert /data/helm/helm/config/cert.pem --tiller-tls-key /data/helm/helm/config/key.pem --service-account tiller
EOS
end
end
diff --git a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
index f28941ce58f..2b7e3ea6def 100644
--- a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
@@ -5,6 +5,8 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
let(:repository) { 'https://repository.example.com' }
let(:rbac) { false }
let(:version) { '1.2.3' }
+ let(:preinstall) { nil }
+ let(:postinstall) { nil }
let(:install_command) do
described_class.new(
@@ -13,7 +15,9 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
rbac: rbac,
files: files,
version: version,
- repository: repository
+ repository: repository,
+ preinstall: preinstall,
+ postinstall: postinstall
)
end
@@ -22,8 +26,9 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
+ helm init --client-only
helm repo add app-name https://repository.example.com
+ helm repo update
#{helm_install_comand}
EOS
end
@@ -38,7 +43,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
--tls-key /data/helm/app-name/config/key.pem
--version 1.2.3
--namespace gitlab-managed-apps
- -f /data/helm/app-name/config/values.yaml >/dev/null
+ -f /data/helm/app-name/config/values.yaml
EOS
end
end
@@ -49,8 +54,9 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
+ helm init --client-only
helm repo add app-name https://repository.example.com
+ helm repo update
#{helm_install_command}
EOS
end
@@ -66,7 +72,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
--version 1.2.3
--set rbac.create\\=true,rbac.enabled\\=true
--namespace gitlab-managed-apps
- -f /data/helm/app-name/config/values.yaml >/dev/null
+ -f /data/helm/app-name/config/values.yaml
EOS
end
end
@@ -78,7 +84,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
+ helm init --client-only
#{helm_install_command}
EOS
end
@@ -93,7 +99,53 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
--tls-key /data/helm/app-name/config/key.pem
--version 1.2.3
--namespace gitlab-managed-apps
- -f /data/helm/app-name/config/values.yaml >/dev/null
+ -f /data/helm/app-name/config/values.yaml
+ EOS
+ end
+ end
+ end
+
+ context 'when there is a pre-install script' do
+ let(:preinstall) { ['/bin/date', '/bin/true'] }
+
+ it_behaves_like 'helm commands' do
+ let(:commands) do
+ <<~EOS
+ helm init --client-only
+ helm repo add app-name https://repository.example.com
+ helm repo update
+ #{helm_install_command}
+ EOS
+ end
+
+ let(:helm_install_command) do
+ <<~EOS.strip
+ /bin/date
+ /bin/true
+ helm install chart-name --name 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 --version 1.2.3 --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml
+ EOS
+ end
+ end
+ end
+
+ context 'when there is a post-install script' do
+ let(:postinstall) { ['/bin/date', "/bin/false\n"] }
+
+ it_behaves_like 'helm commands' do
+ let(:commands) do
+ <<~EOS
+ helm init --client-only
+ helm repo add app-name https://repository.example.com
+ helm repo update
+ #{helm_install_command}
+ EOS
+ end
+
+ let(:helm_install_command) do
+ <<~EOS.strip
+ helm install chart-name --name 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 --version 1.2.3 --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml
+ /bin/date
+ /bin/false
EOS
end
end
@@ -105,8 +157,9 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
+ helm init --client-only
helm repo add app-name https://repository.example.com
+ helm repo update
#{helm_install_command}
EOS
end
@@ -117,7 +170,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
--name app-name
--version 1.2.3
--namespace gitlab-managed-apps
- -f /data/helm/app-name/config/values.yaml >/dev/null
+ -f /data/helm/app-name/config/values.yaml
EOS
end
end
@@ -129,8 +182,9 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
+ helm init --client-only
helm repo add app-name https://repository.example.com
+ helm repo update
#{helm_install_command}
EOS
end
@@ -144,7 +198,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
--tls-cert /data/helm/app-name/config/cert.pem
--tls-key /data/helm/app-name/config/key.pem
--namespace gitlab-managed-apps
- -f /data/helm/app-name/config/values.yaml >/dev/null
+ -f /data/helm/app-name/config/values.yaml
EOS
end
end
diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
index b333b334f36..c92bc92c42d 100644
--- a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
@@ -30,7 +30,7 @@ describe Gitlab::Kubernetes::Helm::Pod do
it 'should generate the appropriate specifications for the container' do
container = subject.generate.spec.containers.first
expect(container.name).to eq('helm')
- expect(container.image).to eq('alpine:3.6')
+ expect(container.image).to eq('registry.gitlab.com/gitlab-org/cluster-integration/helm-install-image/releases/2.7.2-kube-1.11.0')
expect(container.env.count).to eq(3)
expect(container.env.map(&:name)).to match_array([:HELM_VERSION, :TILLER_NAMESPACE, :COMMAND_SCRIPT])
expect(container.command).to match_array(["/bin/sh"])
diff --git a/spec/lib/gitlab/kubernetes/helm/upgrade_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/upgrade_command_spec.rb
index 3dabf04413e..9c9fc91ef3c 100644
--- a/spec/lib/gitlab/kubernetes/helm/upgrade_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/upgrade_command_spec.rb
@@ -21,8 +21,8 @@ describe Gitlab::Kubernetes::Helm::UpgradeCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
- helm upgrade #{application.name} #{application.chart} --tls --tls-ca-cert /data/helm/#{application.name}/config/ca.pem --tls-cert /data/helm/#{application.name}/config/cert.pem --tls-key /data/helm/#{application.name}/config/key.pem --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null
+ helm init --client-only
+ helm upgrade #{application.name} #{application.chart} --tls --tls-ca-cert /data/helm/#{application.name}/config/ca.pem --tls-cert /data/helm/#{application.name}/config/cert.pem --tls-key /data/helm/#{application.name}/config/key.pem --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml
EOS
end
end
@@ -33,8 +33,8 @@ describe Gitlab::Kubernetes::Helm::UpgradeCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
- helm upgrade #{application.name} #{application.chart} --tls --tls-ca-cert /data/helm/#{application.name}/config/ca.pem --tls-cert /data/helm/#{application.name}/config/cert.pem --tls-key /data/helm/#{application.name}/config/key.pem --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null
+ helm init --client-only
+ helm upgrade #{application.name} #{application.chart} --tls --tls-ca-cert /data/helm/#{application.name}/config/ca.pem --tls-cert /data/helm/#{application.name}/config/cert.pem --tls-key /data/helm/#{application.name}/config/key.pem --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml
EOS
end
end
@@ -56,9 +56,9 @@ describe Gitlab::Kubernetes::Helm::UpgradeCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
+ helm init --client-only
helm repo add #{application.name} #{application.repository}
- helm upgrade #{application.name} #{application.chart} --tls --tls-ca-cert /data/helm/#{application.name}/config/ca.pem --tls-cert /data/helm/#{application.name}/config/cert.pem --tls-key /data/helm/#{application.name}/config/key.pem --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null
+ helm upgrade #{application.name} #{application.chart} --tls --tls-ca-cert /data/helm/#{application.name}/config/ca.pem --tls-cert /data/helm/#{application.name}/config/cert.pem --tls-key /data/helm/#{application.name}/config/key.pem --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml
EOS
end
end
@@ -70,8 +70,8 @@ describe Gitlab::Kubernetes::Helm::UpgradeCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
- helm upgrade #{application.name} #{application.chart} --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null
+ helm init --client-only
+ helm upgrade #{application.name} #{application.chart} --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml
EOS
end
end
diff --git a/spec/lib/gitlab/kubernetes/kube_client_spec.rb b/spec/lib/gitlab/kubernetes/kube_client_spec.rb
index eed4135d8a2..3979a43216c 100644
--- a/spec/lib/gitlab/kubernetes/kube_client_spec.rb
+++ b/spec/lib/gitlab/kubernetes/kube_client_spec.rb
@@ -66,6 +66,20 @@ describe Gitlab::Kubernetes::KubeClient do
end
end
+ describe '#knative_client' do
+ subject { client.knative_client }
+
+ it_behaves_like 'a Kubeclient'
+
+ it 'has the extensions API group endpoint' do
+ expect(subject.api_endpoint.to_s).to match(%r{\/apis\/serving.knative.dev\Z})
+ end
+
+ it 'has the api_version' do
+ expect(subject.instance_variable_get(:@api_version)).to eq('v1alpha1')
+ end
+ end
+
describe 'core API' do
let(:core_client) { client.core_client }
diff --git a/spec/lib/gitlab/kubernetes/namespace_spec.rb b/spec/lib/gitlab/kubernetes/namespace_spec.rb
index e098612f6fb..e1c35c355f4 100644
--- a/spec/lib/gitlab/kubernetes/namespace_spec.rb
+++ b/spec/lib/gitlab/kubernetes/namespace_spec.rb
@@ -9,7 +9,7 @@ describe Gitlab::Kubernetes::Namespace do
describe '#exists?' do
context 'when namespace do not exits' do
- let(:exception) { ::Kubeclient::HttpError.new(404, "namespace #{name} not found", nil) }
+ let(:exception) { ::Kubeclient::ResourceNotFoundError.new(404, "namespace #{name} not found", nil) }
it 'returns false' do
expect(client).to receive(:get_namespace).with(name).once.and_raise(exception)
diff --git a/spec/lib/gitlab/kubernetes/role_binding_spec.rb b/spec/lib/gitlab/kubernetes/role_binding_spec.rb
index da3f5d27b25..a1a59533bfb 100644
--- a/spec/lib/gitlab/kubernetes/role_binding_spec.rb
+++ b/spec/lib/gitlab/kubernetes/role_binding_spec.rb
@@ -20,7 +20,7 @@ describe Gitlab::Kubernetes::RoleBinding, '#generate' do
let(:role_ref) do
{
apiGroup: 'rbac.authorization.k8s.io',
- kind: 'Role',
+ kind: 'ClusterRole',
name: role_name
}
end
@@ -35,6 +35,7 @@ describe Gitlab::Kubernetes::RoleBinding, '#generate' do
subject do
described_class.new(
+ name: "gitlab-#{namespace}",
role_name: role_name,
namespace: namespace,
service_account_name: service_account_name
diff --git a/spec/lib/gitlab/private_commit_email_spec.rb b/spec/lib/gitlab/private_commit_email_spec.rb
new file mode 100644
index 00000000000..bc86cd3842a
--- /dev/null
+++ b/spec/lib/gitlab/private_commit_email_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::PrivateCommitEmail do
+ let(:hostname) { Gitlab::CurrentSettings.current_application_settings.commit_email_hostname }
+
+ context '.regex' do
+ subject { described_class.regex }
+
+ it { is_expected.to match("1-foo@#{hostname}") }
+ it { is_expected.not_to match("1-foo@#{hostname}.foo") }
+ it { is_expected.not_to match('1-foo@users.noreply.gitlab.com') }
+ it { is_expected.not_to match('foo-1@users.noreply.gitlab.com') }
+ it { is_expected.not_to match('foobar@gitlab.com') }
+ end
+
+ context '.user_id_for_email' do
+ let(:id) { 1 }
+
+ it 'parses user id from email' do
+ email = "#{id}-foo@#{hostname}"
+
+ expect(described_class.user_id_for_email(email)).to eq(id)
+ end
+
+ it 'returns nil on invalid commit email' do
+ email = "#{id}-foo@users.noreply.bar.com"
+
+ expect(described_class.user_id_for_email(email)).to be_nil
+ end
+ end
+
+ context '.for_user' do
+ it 'returns email in the format id-username@hostname' do
+ user = create(:user)
+
+ expect(described_class.for_user(user)).to eq("#{user.id}-#{user.username}@#{hostname}")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/quick_actions/command_definition_spec.rb b/spec/lib/gitlab/quick_actions/command_definition_spec.rb
index b03c1e23ca3..5dae82a63b4 100644
--- a/spec/lib/gitlab/quick_actions/command_definition_spec.rb
+++ b/spec/lib/gitlab/quick_actions/command_definition_spec.rb
@@ -210,6 +210,19 @@ describe Gitlab::QuickActions::CommandDefinition do
end
end
+ context 'when warning is set' do
+ before do
+ subject.explanation = 'Explanation'
+ subject.warning = 'dangerous!'
+ end
+
+ it 'returns this static string' do
+ result = subject.explain({}, nil)
+
+ expect(result).to eq 'Explanation (dangerous!)'
+ end
+ end
+
context 'when the explanation is dynamic' do
before do
subject.explanation = proc { |arg| "Dynamic #{arg}" }
diff --git a/spec/lib/gitlab/quick_actions/dsl_spec.rb b/spec/lib/gitlab/quick_actions/dsl_spec.rb
index 067a30fd7e2..fd4df8694ba 100644
--- a/spec/lib/gitlab/quick_actions/dsl_spec.rb
+++ b/spec/lib/gitlab/quick_actions/dsl_spec.rb
@@ -12,6 +12,7 @@ describe Gitlab::QuickActions::Dsl do
params 'The first argument'
explanation 'Static explanation'
+ warning 'Possible problem!'
command :explanation_with_aliases, :once, :first do |arg|
arg
end
@@ -64,6 +65,7 @@ describe Gitlab::QuickActions::Dsl do
expect(no_args_def.condition_block).to be_nil
expect(no_args_def.action_block).to be_a_kind_of(Proc)
expect(no_args_def.parse_params_block).to be_nil
+ expect(no_args_def.warning).to eq('')
expect(explanation_with_aliases_def.name).to eq(:explanation_with_aliases)
expect(explanation_with_aliases_def.aliases).to eq([:once, :first])
@@ -73,6 +75,7 @@ describe Gitlab::QuickActions::Dsl do
expect(explanation_with_aliases_def.condition_block).to be_nil
expect(explanation_with_aliases_def.action_block).to be_a_kind_of(Proc)
expect(explanation_with_aliases_def.parse_params_block).to be_nil
+ expect(explanation_with_aliases_def.warning).to eq('Possible problem!')
expect(dynamic_description_def.name).to eq(:dynamic_description)
expect(dynamic_description_def.aliases).to eq([])
@@ -82,6 +85,7 @@ describe Gitlab::QuickActions::Dsl do
expect(dynamic_description_def.condition_block).to be_nil
expect(dynamic_description_def.action_block).to be_a_kind_of(Proc)
expect(dynamic_description_def.parse_params_block).to be_nil
+ expect(dynamic_description_def.warning).to eq('')
expect(cc_def.name).to eq(:cc)
expect(cc_def.aliases).to eq([])
@@ -91,6 +95,7 @@ describe Gitlab::QuickActions::Dsl do
expect(cc_def.condition_block).to be_nil
expect(cc_def.action_block).to be_nil
expect(cc_def.parse_params_block).to be_nil
+ expect(cc_def.warning).to eq('')
expect(cond_action_def.name).to eq(:cond_action)
expect(cond_action_def.aliases).to eq([])
@@ -100,6 +105,7 @@ describe Gitlab::QuickActions::Dsl do
expect(cond_action_def.condition_block).to be_a_kind_of(Proc)
expect(cond_action_def.action_block).to be_a_kind_of(Proc)
expect(cond_action_def.parse_params_block).to be_nil
+ expect(cond_action_def.warning).to eq('')
expect(with_params_parsing_def.name).to eq(:with_params_parsing)
expect(with_params_parsing_def.aliases).to eq([])
@@ -109,6 +115,7 @@ describe Gitlab::QuickActions::Dsl do
expect(with_params_parsing_def.condition_block).to be_nil
expect(with_params_parsing_def.action_block).to be_a_kind_of(Proc)
expect(with_params_parsing_def.parse_params_block).to be_a_kind_of(Proc)
+ expect(with_params_parsing_def.warning).to eq('')
expect(substitution_def.name).to eq(:something)
expect(substitution_def.aliases).to eq([])
@@ -118,6 +125,7 @@ describe Gitlab::QuickActions::Dsl do
expect(substitution_def.condition_block).to be_nil
expect(substitution_def.action_block.call('text')).to eq('text Some complicated thing you want in here')
expect(substitution_def.parse_params_block).to be_nil
+ expect(substitution_def.warning).to eq('')
end
end
end
diff --git a/spec/lib/gitlab/quick_actions/extractor_spec.rb b/spec/lib/gitlab/quick_actions/extractor_spec.rb
index 0166f6c2ee0..873bb359d6e 100644
--- a/spec/lib/gitlab/quick_actions/extractor_spec.rb
+++ b/spec/lib/gitlab/quick_actions/extractor_spec.rb
@@ -272,5 +272,24 @@ describe Gitlab::QuickActions::Extractor do
expect(commands).to be_empty
expect(msg).to eq expected
end
+
+ it 'limits to passed commands when they are passed' do
+ msg = <<~MSG.strip
+ Hello, we should only extract the commands passed
+ /reopen
+ /labels hello world
+ /power
+ MSG
+ expected_msg = <<~EXPECTED.strip
+ Hello, we should only extract the commands passed
+ /power
+ EXPECTED
+ expected_commands = [['reopen'], ['labels', 'hello world']]
+
+ msg, commands = extractor.extract_commands(msg, only: [:open, :labels])
+
+ expect(commands).to eq(expected_commands)
+ expect(msg).to eq expected_msg
+ end
end
end
diff --git a/spec/lib/gitlab/sentry_spec.rb b/spec/lib/gitlab/sentry_spec.rb
index 499757da061..d3b41b27b80 100644
--- a/spec/lib/gitlab/sentry_spec.rb
+++ b/spec/lib/gitlab/sentry_spec.rb
@@ -52,4 +52,28 @@ describe Gitlab::Sentry do
end
end
end
+
+ context '.track_acceptable_exception' do
+ let(:exception) { RuntimeError.new('boom') }
+
+ before do
+ allow(described_class).to receive(:enabled?).and_return(true)
+ end
+
+ it 'calls Raven.capture_exception' do
+ expected_extras = {
+ some_other_info: 'info',
+ issue_url: 'http://gitlab.com/gitlab-org/gitlab-ce/issues/1'
+ }
+
+ expect(Raven).to receive(:capture_exception)
+ .with(exception, extra: a_hash_including(expected_extras))
+
+ described_class.track_acceptable_exception(
+ exception,
+ issue_url: 'http://gitlab.com/gitlab-org/gitlab-ce/issues/1',
+ extra: { some_other_info: 'info' }
+ )
+ end
+ end
end
diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb
index b1b7c427313..6ce9d515a0f 100644
--- a/spec/lib/gitlab/shell_spec.rb
+++ b/spec/lib/gitlab/shell_spec.rb
@@ -498,126 +498,6 @@ describe Gitlab::Shell do
end
end
- describe '#fetch_remote' do
- def fetch_remote(ssh_auth = nil, prune = true)
- gitlab_shell.fetch_remote(repository.raw_repository, 'remote-name', ssh_auth: ssh_auth, prune: prune)
- end
-
- def expect_call(fail, options = {})
- receive_fetch_remote =
- if fail
- receive(:fetch_remote).and_raise(GRPC::NotFound)
- else
- receive(:fetch_remote).and_return(true)
- end
-
- expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive_fetch_remote
- end
-
- def build_ssh_auth(opts = {})
- defaults = {
- ssh_import?: true,
- ssh_key_auth?: false,
- ssh_known_hosts: nil,
- ssh_private_key: nil
- }
-
- double(:ssh_auth, defaults.merge(opts))
- end
-
- it 'returns true when the command succeeds' do
- expect_call(false, force: false, tags: true, prune: true)
-
- expect(fetch_remote).to be_truthy
- end
-
- it 'returns true when the command succeeds' do
- expect_call(false, force: false, tags: true, prune: false)
-
- expect(fetch_remote(nil, false)).to be_truthy
- end
-
- it 'raises an exception when the command fails' do
- expect_call(true, force: false, tags: true, prune: true)
-
- expect { fetch_remote }.to raise_error(Gitlab::Shell::Error)
- end
-
- it 'allows forced and no_tags to be changed' do
- expect_call(false, force: true, tags: false, prune: true)
-
- result = gitlab_shell.fetch_remote(repository.raw_repository, 'remote-name', forced: true, no_tags: true, prune: true)
- expect(result).to be_truthy
- end
-
- context 'SSH auth' do
- it 'passes the SSH key if specified' do
- expect_call(false, force: false, tags: true, prune: true, ssh_key: 'foo')
-
- ssh_auth = build_ssh_auth(ssh_key_auth?: true, ssh_private_key: 'foo')
-
- expect(fetch_remote(ssh_auth)).to be_truthy
- end
-
- it 'does not pass an empty SSH key' do
- expect_call(false, force: false, tags: true, prune: true)
-
- ssh_auth = build_ssh_auth(ssh_key_auth: true, ssh_private_key: '')
-
- expect(fetch_remote(ssh_auth)).to be_truthy
- end
-
- it 'does not pass the key unless SSH key auth is to be used' do
- expect_call(false, force: false, tags: true, prune: true)
-
- ssh_auth = build_ssh_auth(ssh_key_auth: false, ssh_private_key: 'foo')
-
- expect(fetch_remote(ssh_auth)).to be_truthy
- end
-
- it 'passes the known_hosts data if specified' do
- expect_call(false, force: false, tags: true, prune: true, known_hosts: 'foo')
-
- ssh_auth = build_ssh_auth(ssh_known_hosts: 'foo')
-
- expect(fetch_remote(ssh_auth)).to be_truthy
- end
-
- it 'does not pass empty known_hosts data' do
- expect_call(false, force: false, tags: true, prune: true)
-
- ssh_auth = build_ssh_auth(ssh_known_hosts: '')
-
- expect(fetch_remote(ssh_auth)).to be_truthy
- end
-
- it 'does not pass known_hosts data unless SSH is to be used' do
- expect_call(false, force: false, tags: true, prune: true)
-
- ssh_auth = build_ssh_auth(ssh_import?: false, ssh_known_hosts: 'foo')
-
- expect(fetch_remote(ssh_auth)).to be_truthy
- end
- end
-
- context 'gitaly call' do
- let(:remote_name) { 'remote-name' }
- let(:ssh_auth) { double(:ssh_auth) }
-
- subject do
- gitlab_shell.fetch_remote(repository.raw_repository, remote_name,
- forced: true, no_tags: true, ssh_auth: ssh_auth)
- end
-
- it 'passes the correct params to the gitaly service' do
- expect(repository.gitaly_repository_client).to receive(:fetch_remote)
- .with(remote_name, ssh_auth: ssh_auth, forced: true, no_tags: true, prune: true, timeout: timeout)
-
- subject
- end
- end
- end
-
describe '#import_repository' do
let(:import_url) { 'https://gitlab.com/gitlab-org/gitlab-ce.git' }
diff --git a/spec/lib/gitlab/slash_commands/command_spec.rb b/spec/lib/gitlab/slash_commands/command_spec.rb
index 194cae8c645..eceacac58af 100644
--- a/spec/lib/gitlab/slash_commands/command_spec.rb
+++ b/spec/lib/gitlab/slash_commands/command_spec.rb
@@ -44,7 +44,7 @@ describe Gitlab::SlashCommands::Command do
let!(:build) { create(:ci_build, pipeline: pipeline) }
let!(:pipeline) { create(:ci_pipeline, project: project) }
let!(:staging) { create(:environment, name: 'staging', project: project) }
- let!(:deployment) { create(:deployment, environment: staging, deployable: build) }
+ let!(:deployment) { create(:deployment, :success, environment: staging, deployable: build) }
let!(:manual) do
create(:ci_build, :manual, pipeline: pipeline,
diff --git a/spec/lib/gitlab/slash_commands/deploy_spec.rb b/spec/lib/gitlab/slash_commands/deploy_spec.rb
index 0d57334aa4c..25f3e8a0409 100644
--- a/spec/lib/gitlab/slash_commands/deploy_spec.rb
+++ b/spec/lib/gitlab/slash_commands/deploy_spec.rb
@@ -31,7 +31,7 @@ describe Gitlab::SlashCommands::Deploy do
let!(:staging) { create(:environment, name: 'staging', project: project) }
let!(:pipeline) { create(:ci_pipeline, project: project) }
let!(:build) { create(:ci_build, pipeline: pipeline) }
- let!(:deployment) { create(:deployment, environment: staging, deployable: build) }
+ let!(:deployment) { create(:deployment, :success, environment: staging, deployable: build) }
context 'without actions' do
it 'does not execute an action' do
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 69ee5ff4bcd..b212d2b05f2 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -8,6 +8,7 @@ describe Gitlab::UsageData do
before do
create(:jira_service, project: projects[0])
create(:jira_service, project: projects[1])
+ create(:jira_cloud_service, project: projects[2])
create(:prometheus_service, project: projects[1])
create(:service, project: projects[0], type: 'SlackSlashCommandsService', active: true)
create(:service, project: projects[1], type: 'SlackService', active: true)
@@ -20,6 +21,7 @@ describe Gitlab::UsageData do
create(:clusters_applications_ingress, :installed, cluster: gcp_cluster)
create(:clusters_applications_prometheus, :installed, cluster: gcp_cluster)
create(:clusters_applications_runner, :installed, cluster: gcp_cluster)
+ create(:clusters_applications_knative, :installed, cluster: gcp_cluster)
end
subject { described_class.data }
@@ -81,6 +83,7 @@ describe Gitlab::UsageData do
clusters_applications_ingress
clusters_applications_prometheus
clusters_applications_runner
+ clusters_applications_knative
in_review_folder
groups
issues
@@ -95,6 +98,8 @@ describe Gitlab::UsageData do
projects
projects_imported_from_github
projects_jira_active
+ projects_jira_server_active
+ projects_jira_cloud_active
projects_slack_notifications_active
projects_slack_slash_active
projects_prometheus_active
@@ -114,7 +119,9 @@ describe Gitlab::UsageData do
expect(count_data[:projects]).to eq(3)
expect(count_data[:projects_prometheus_active]).to eq(1)
- expect(count_data[:projects_jira_active]).to eq(2)
+ expect(count_data[:projects_jira_active]).to eq(3)
+ expect(count_data[:projects_jira_server_active]).to eq(2)
+ expect(count_data[:projects_jira_cloud_active]).to eq(1)
expect(count_data[:projects_slack_notifications_active]).to eq(2)
expect(count_data[:projects_slack_slash_active]).to eq(1)
@@ -126,6 +133,7 @@ describe Gitlab::UsageData do
expect(count_data[:clusters_applications_ingress]).to eq(1)
expect(count_data[:clusters_applications_prometheus]).to eq(1)
expect(count_data[:clusters_applications_runner]).to eq(1)
+ expect(count_data[:clusters_applications_knative]).to eq(1)
end
it 'works when queries time out' do
diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb
index 4ba99009855..ad2c9d7f2af 100644
--- a/spec/lib/gitlab/utils_spec.rb
+++ b/spec/lib/gitlab/utils_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Gitlab::Utils do
delegate :to_boolean, :boolean_to_yes_no, :slugify, :random_string, :which, :ensure_array_from_string,
- :bytes_to_megabytes, to: :described_class
+ :bytes_to_megabytes, :append_path, to: :described_class
describe '.slugify' do
{
@@ -106,4 +106,25 @@ describe Gitlab::Utils do
expect(bytes_to_megabytes(bytes)).to eq(1)
end
end
+
+ describe '.append_path' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:host, :path, :result) do
+ 'http://test/' | '/foo/bar' | 'http://test/foo/bar'
+ 'http://test/' | '//foo/bar' | 'http://test/foo/bar'
+ 'http://test//' | '/foo/bar' | 'http://test/foo/bar'
+ 'http://test' | 'foo/bar' | 'http://test/foo/bar'
+ 'http://test//' | '' | 'http://test/'
+ 'http://test//' | nil | 'http://test/'
+ '' | '/foo/bar' | '/foo/bar'
+ nil | '/foo/bar' | '/foo/bar'
+ end
+
+ with_them do
+ it 'makes sure there is only one slash as path separator' do
+ expect(append_path(host, path)).to eq(result)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/view/presenter/base_spec.rb b/spec/lib/gitlab/view/presenter/base_spec.rb
index 4eca53032a2..02c2fd47197 100644
--- a/spec/lib/gitlab/view/presenter/base_spec.rb
+++ b/spec/lib/gitlab/view/presenter/base_spec.rb
@@ -40,7 +40,7 @@ describe Gitlab::View::Presenter::Base do
end
end
- context 'subject is overriden' do
+ context 'subject is overridden' do
it 'returns true' do
presenter = presenter_class.new(build_stubbed(:project, :public))
diff --git a/spec/lib/json_web_token/hmac_token_spec.rb b/spec/lib/json_web_token/hmac_token_spec.rb
new file mode 100644
index 00000000000..f2cbc381967
--- /dev/null
+++ b/spec/lib/json_web_token/hmac_token_spec.rb
@@ -0,0 +1,133 @@
+# frozen_string_literal: true
+
+require 'json'
+require 'timecop'
+
+describe JSONWebToken::HMACToken do
+ let(:secret) { 'shh secret squirrel' }
+
+ shared_examples 'a valid, non-expired token' do
+ it 'is an Array with two elements' do
+ expect(decoded_token).to be_a(Array)
+ expect(decoded_token.count).to eq(2)
+ end
+
+ it 'contains the following keys in the first Array element Hash - jti, iat, nbf, exp' do
+ expect(decoded_token[0].keys).to include('jti', 'iat', 'nbf', 'exp')
+ end
+
+ it 'contains the following keys in the second Array element Hash - typ and alg' do
+ expect(decoded_token[1]['typ']).to eql('JWT')
+ expect(decoded_token[1]['alg']).to eql('HS256')
+ end
+ end
+
+ describe '.decode' do
+ let(:leeway) { described_class::IAT_LEEWAY }
+ let(:decoded_token) { described_class.decode(encoded_token, secret, leeway: leeway) }
+
+ context 'with an invalid token' do
+ context 'that is junk' do
+ let(:encoded_token) { 'junk' }
+
+ it "raises exception saying 'Not enough or too many segments'" do
+ expect { decoded_token }.to raise_error(JWT::DecodeError, 'Not enough or too many segments')
+ end
+ end
+
+ context 'that has been fiddled with' do
+ let(:encoded_token) do
+ described_class.new(secret).encoded.tap { |token| token[0] = 'E' }
+ end
+
+ it "raises exception saying 'Invalid segment encoding'" do
+ expect { decoded_token }.to raise_error(JWT::DecodeError, 'Invalid segment encoding')
+ end
+ end
+
+ context 'that was generated using a different secret' do
+ let(:encoded_token) { described_class.new('some other secret').encoded }
+
+ it "raises exception saying 'Signature verification raised" do
+ expect { decoded_token }.to raise_error(JWT::VerificationError, 'Signature verification raised')
+ end
+ end
+
+ context 'that is expired' do
+ # Needs the ! so Timecop.freeze() is effective
+ let!(:encoded_token) { described_class.new(secret).encoded }
+
+ it "raises exception saying 'Signature has expired'" do
+ # Needs to be 120 seconds, because the default expiry is 60 seconds
+ # with an additional 60 second leeway.
+ Timecop.freeze(Time.now + 120) do
+ expect { decoded_token }.to raise_error(JWT::ExpiredSignature, 'Signature has expired')
+ end
+ end
+ end
+ end
+
+ context 'with a valid token' do
+ let(:encoded_token) do
+ hmac_token = described_class.new(secret)
+ hmac_token.expire_time = Time.now + expire_time
+ hmac_token.encoded
+ end
+
+ context 'that has expired' do
+ let(:expire_time) { 0 }
+
+ context 'with the default leeway' do
+ Timecop.freeze(Time.now + 1) do
+ it_behaves_like 'a valid, non-expired token'
+ end
+ end
+
+ context 'with a leeway of 0 seconds' do
+ let(:leeway) { 0 }
+
+ it "raises exception saying 'Signature has expired'" do
+ Timecop.freeze(Time.now + 1) do
+ expect { decoded_token }.to raise_error(JWT::ExpiredSignature, 'Signature has expired')
+ end
+ end
+ end
+ end
+
+ context 'that has not expired' do
+ let(:expire_time) { described_class::DEFAULT_EXPIRE_TIME }
+
+ it_behaves_like 'a valid, non-expired token'
+ end
+ end
+ end
+
+ describe '#encoded' do
+ let(:decoded_token) { described_class.decode(encoded_token, secret) }
+
+ context 'without data' do
+ let(:encoded_token) { described_class.new(secret).encoded }
+
+ it_behaves_like 'a valid, non-expired token'
+ end
+
+ context 'with data' do
+ let(:data) { { secret_key: 'secret value' }.to_json }
+ let(:encoded_token) do
+ ec = described_class.new(secret)
+ ec[:data] = data
+ ec.encoded
+ end
+
+ it_behaves_like 'a valid, non-expired token'
+
+ it "contains the 'data' key in the first Array element Hash" do
+ expect(decoded_token[0]).to have_key('data')
+ end
+
+ it 'can re-read back the data' do
+ expect(decoded_token[0]['data']).to eql(data)
+ end
+ end
+ end
+end
diff --git a/spec/lib/microsoft_teams/notifier_spec.rb b/spec/lib/microsoft_teams/notifier_spec.rb
index c9756544bd6..2aaa7c24ad8 100644
--- a/spec/lib/microsoft_teams/notifier_spec.rb
+++ b/spec/lib/microsoft_teams/notifier_spec.rb
@@ -48,7 +48,7 @@ describe MicrosoftTeams::Notifier do
stub_request(:post, webhook_url).with(body: JSON(body), headers: { 'Content-Type' => 'application/json' }).to_return(status: 200, body: "", headers: {})
end
- it 'expects to receive successfull answer' do
+ it 'expects to receive successful answer' do
expect(subject.ping(options)).to be true
end
end
diff --git a/spec/migrations/delete_inconsistent_internal_id_records_spec.rb b/spec/migrations/delete_inconsistent_internal_id_records_spec.rb
index becb71cf427..4af51217031 100644
--- a/spec/migrations/delete_inconsistent_internal_id_records_spec.rb
+++ b/spec/migrations/delete_inconsistent_internal_id_records_spec.rb
@@ -65,6 +65,21 @@ describe DeleteInconsistentInternalIdRecords, :migration do
context 'for deployments' do
let(:scope) { :deployment }
+ let(:deployments) { table(:deployments) }
+ let(:internal_ids) { table(:internal_ids) }
+
+ before do
+ internal_ids.create!(project_id: project1.id, usage: 2, last_value: 2)
+ internal_ids.create!(project_id: project2.id, usage: 2, last_value: 2)
+ internal_ids.create!(project_id: project3.id, usage: 2, last_value: 2)
+ end
+
+ let(:create_models) do
+ 3.times { |i| deployments.create!(project_id: project1.id, iid: i, environment_id: 1, ref: 'master', sha: 'a', tag: false) }
+ 3.times { |i| deployments.create!(project_id: project2.id, iid: i, environment_id: 1, ref: 'master', sha: 'a', tag: false) }
+ 3.times { |i| deployments.create!(project_id: project3.id, iid: i, environment_id: 1, ref: 'master', sha: 'a', tag: false) }
+ end
+
it_behaves_like 'deleting inconsistent internal_id records'
end
diff --git a/spec/migrations/fill_empty_finished_at_in_deployments_spec.rb b/spec/migrations/fill_empty_finished_at_in_deployments_spec.rb
new file mode 100644
index 00000000000..cf5c10f77e1
--- /dev/null
+++ b/spec/migrations/fill_empty_finished_at_in_deployments_spec.rb
@@ -0,0 +1,70 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20181030135124_fill_empty_finished_at_in_deployments')
+
+describe FillEmptyFinishedAtInDeployments, :migration do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:environments) { table(:environments) }
+ let(:deployments) { table(:deployments) }
+
+ context 'when a deployment row does not have a value on finished_at' do
+ context 'when a deployment succeeded' do
+ before do
+ namespaces.create!(id: 123, name: 'gitlab1', path: 'gitlab1')
+ projects.create!(id: 1, name: 'gitlab1', path: 'gitlab1', namespace_id: 123)
+ environments.create!(id: 1, name: 'production', slug: 'production', project_id: 1)
+ deployments.create!(id: 1, iid: 1, project_id: 1, environment_id: 1, ref: 'master', sha: 'xxx', tag: false)
+ end
+
+ it 'correctly replicates finished_at by created_at' do
+ expect(deployments.last.created_at).not_to be_nil
+ expect(deployments.last.finished_at).to be_nil
+
+ migrate!
+
+ expect(deployments.last.created_at).not_to be_nil
+ expect(deployments.last.finished_at).to eq(deployments.last.created_at)
+ end
+ end
+
+ context 'when a deployment is running' do
+ before do
+ namespaces.create!(id: 123, name: 'gitlab1', path: 'gitlab1')
+ projects.create!(id: 1, name: 'gitlab1', path: 'gitlab1', namespace_id: 123)
+ environments.create!(id: 1, name: 'production', slug: 'production', project_id: 1)
+ deployments.create!(id: 1, iid: 1, project_id: 1, environment_id: 1, ref: 'master', sha: 'xxx', tag: false, status: 1)
+ end
+
+ it 'does not fill finished_at' do
+ expect(deployments.last.created_at).not_to be_nil
+ expect(deployments.last.finished_at).to be_nil
+
+ migrate!
+
+ expect(deployments.last.created_at).not_to be_nil
+ expect(deployments.last.finished_at).to be_nil
+ end
+ end
+ end
+
+ context 'when a deployment row does has a value on finished_at' do
+ let(:finished_at) { '2018-10-30 11:12:02 UTC' }
+
+ before do
+ namespaces.create!(id: 123, name: 'gitlab1', path: 'gitlab1')
+ projects.create!(id: 1, name: 'gitlab1', path: 'gitlab1', namespace_id: 123)
+ environments.create!(id: 1, name: 'production', slug: 'production', project_id: 1)
+ deployments.create!(id: 1, iid: 1, project_id: 1, environment_id: 1, ref: 'master', sha: 'xxx', tag: false, finished_at: finished_at)
+ end
+
+ it 'does not affect existing value' do
+ expect(deployments.last.created_at).not_to be_nil
+ expect(deployments.last.finished_at).not_to be_nil
+
+ migrate!
+
+ expect(deployments.last.created_at).not_to be_nil
+ expect(deployments.last.finished_at).to eq(finished_at)
+ end
+ end
+end
diff --git a/spec/migrations/migrate_old_artifacts_spec.rb b/spec/migrations/migrate_old_artifacts_spec.rb
index 4187ab149a5..af77d64fdbf 100644
--- a/spec/migrations/migrate_old_artifacts_spec.rb
+++ b/spec/migrations/migrate_old_artifacts_spec.rb
@@ -76,7 +76,7 @@ describe MigrateOldArtifacts do
end
end
- context 'when there are aritfacts in old and new directory' do
+ context 'when there are artifacts in old and new directory' do
before do
store_artifacts_in_legacy_path(build2)
diff --git a/spec/migrations/steal_fill_store_upload_spec.rb b/spec/migrations/steal_fill_store_upload_spec.rb
new file mode 100644
index 00000000000..ed809baf2b5
--- /dev/null
+++ b/spec/migrations/steal_fill_store_upload_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20181105201455_steal_fill_store_upload.rb')
+
+describe StealFillStoreUpload, :migration do
+ let(:uploads) { table(:uploads) }
+
+ describe '#up' do
+ it 'steals the FillStoreUpload background migration' do
+ expect(Gitlab::BackgroundMigration).to receive(:steal).with('FillStoreUpload').and_call_original
+
+ migrate!
+ end
+
+ it 'does not run migration if not needed' do
+ uploads.create(size: 100.kilobytes,
+ uploader: 'AvatarUploader',
+ path: 'uploads/-/system/avatar.jpg',
+ store: 1)
+
+ expect_any_instance_of(Gitlab::BackgroundMigration::FillStoreUpload).not_to receive(:perform)
+
+ migrate!
+ end
+
+ it 'ensures all rows are migrated' do
+ uploads.create(size: 100.kilobytes,
+ uploader: 'AvatarUploader',
+ path: 'uploads/-/system/avatar.jpg',
+ store: nil)
+
+ expect_any_instance_of(Gitlab::BackgroundMigration::FillStoreUpload).to receive(:perform).and_call_original
+
+ expect do
+ migrate!
+ end.to change { uploads.where(store: nil).count }.from(1).to(0)
+ end
+ end
+end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 87b91286168..96aa9a82b71 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -25,6 +25,9 @@ describe ApplicationSetting do
it { is_expected.to allow_value(https).for(:after_sign_out_path) }
it { is_expected.not_to allow_value(ftp).for(:after_sign_out_path) }
+ 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) }
+
describe 'default_artifacts_expire_in' do
it 'sets an error if it cannot parse' do
setting.update(default_artifacts_expire_in: 'a')
@@ -107,6 +110,14 @@ describe ApplicationSetting do
it { expect(setting.repository_storages).to eq(['default']) }
end
+ context '#commit_email_hostname' do
+ it 'returns configured gitlab hostname if commit_email_hostname is not defined' do
+ setting.update(commit_email_hostname: nil)
+
+ expect(setting.commit_email_hostname).to eq("users.noreply.#{Gitlab.config.gitlab.host}")
+ end
+ end
+
context 'auto_devops_domain setting' do
context 'when auto_devops_enabled? is true' do
before do
@@ -594,4 +605,24 @@ describe ApplicationSetting do
end
end
end
+
+ describe '#archive_builds_older_than' do
+ subject { setting.archive_builds_older_than }
+
+ context 'when the archive_builds_in_seconds is set' do
+ before do
+ setting.archive_builds_in_seconds = 3600
+ end
+
+ it { is_expected.to be_within(1.minute).of(1.hour.ago) }
+ end
+
+ context 'when the archive_builds_in_seconds is set' do
+ before do
+ setting.archive_builds_in_seconds = nil
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 65e06f27f35..6849bc6db7a 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -17,8 +17,8 @@ describe Ci::Build do
it { is_expected.to belong_to(:runner) }
it { is_expected.to belong_to(:trigger_request) }
it { is_expected.to belong_to(:erased_by) }
- it { is_expected.to have_many(:deployments) }
it { is_expected.to have_many(:trace_sections)}
+ it { is_expected.to have_one(:deployment) }
it { is_expected.to have_one(:runner_session)}
it { is_expected.to validate_presence_of(:ref) }
it { is_expected.to respond_to(:has_trace?) }
@@ -216,14 +216,6 @@ describe Ci::Build do
let(:build) { create(:ci_build, :created, :schedulable, project: project) }
it { expect(subject).to be_truthy }
-
- context 'when feature flag is diabled' do
- before do
- stub_feature_flags(ci_enable_scheduled_build: false)
- end
-
- it { expect(subject).to be_falsy }
- end
end
context 'when build is not schedulable' do
@@ -327,10 +319,6 @@ describe Ci::Build do
describe '#enqueue_scheduled' do
subject { build.enqueue_scheduled }
- before do
- stub_feature_flags(ci_enable_scheduled_build: true)
- end
-
context 'when build is scheduled and the right time has not come yet' do
let(:build) { create(:ci_build, :scheduled, pipeline: pipeline) }
@@ -811,17 +799,100 @@ describe Ci::Build do
end
end
+ describe 'state transition as a deployable' do
+ let!(:build) { create(:ci_build, :start_review_app) }
+ let(:deployment) { build.deployment }
+ let(:environment) { deployment.environment }
+
+ it 'has deployments record with created status' do
+ expect(deployment).to be_created
+ expect(environment.name).to eq('review/master')
+ end
+
+ context 'when transits to running' do
+ before do
+ build.run!
+ end
+
+ it 'transits deployment status to running' do
+ expect(deployment).to be_running
+ end
+ end
+
+ context 'when transits to success' do
+ before do
+ allow(Deployments::SuccessWorker).to receive(:perform_async)
+ build.success!
+ end
+
+ it 'transits deployment status to success' do
+ expect(deployment).to be_success
+ end
+ end
+
+ context 'when transits to failed' do
+ before do
+ build.drop!
+ end
+
+ it 'transits deployment status to failed' do
+ expect(deployment).to be_failed
+ end
+ end
+
+ context 'when transits to skipped' do
+ before do
+ build.skip!
+ end
+
+ it 'transits deployment status to canceled' do
+ expect(deployment).to be_canceled
+ end
+ end
+
+ context 'when transits to canceled' do
+ before do
+ build.cancel!
+ end
+
+ it 'transits deployment status to canceled' do
+ expect(deployment).to be_canceled
+ end
+ end
+ end
+
+ describe '#on_stop' do
+ subject { build.on_stop }
+
+ context 'when a job has a specification that it can be stopped from the other job' do
+ let(:build) { create(:ci_build, :start_review_app) }
+
+ it 'returns the other job name' do
+ is_expected.to eq('stop_review_app')
+ end
+ end
+
+ context 'when a job does not have environment information' do
+ let(:build) { create(:ci_build) }
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
describe 'deployment' do
- describe '#last_deployment' do
- subject { build.last_deployment }
+ describe '#has_deployment?' do
+ subject { build.has_deployment? }
- context 'when multiple deployments are created' do
- let!(:deployment1) { create(:deployment, deployable: build) }
- let!(:deployment2) { create(:deployment, deployable: build) }
+ context 'when build has a deployment' do
+ let!(:deployment) { create(:deployment, deployable: build) }
- it 'returns the latest one' do
- is_expected.to eq(deployment2)
- end
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when build does not have a deployment' do
+ it { is_expected.to be_falsy }
end
end
@@ -830,14 +901,14 @@ describe Ci::Build do
context 'when build succeeded' do
let(:build) { create(:ci_build, :success) }
- let!(:deployment) { create(:deployment, deployable: build) }
+ let!(:deployment) { create(:deployment, :success, deployable: build) }
context 'current deployment is latest' do
it { is_expected.to be_falsey }
end
context 'current deployment is not latest on environment' do
- let!(:deployment2) { create(:deployment, environment: deployment.environment) }
+ let!(:deployment2) { create(:deployment, :success, environment: deployment.environment) }
it { is_expected.to be_truthy }
end
@@ -1326,6 +1397,14 @@ describe Ci::Build do
it { is_expected.not_to be_retryable }
end
+
+ context 'when build is degenerated' do
+ before do
+ build.degenerate!
+ end
+
+ it { is_expected.not_to be_retryable }
+ end
end
end
@@ -1393,21 +1472,127 @@ describe Ci::Build do
end
describe '#retries_max' do
- context 'when max retries value is defined' do
- subject { create(:ci_build, options: { retry: 1 }) }
+ context 'with retries max config option' do
+ subject { create(:ci_build, options: { retry: { max: 1 } }) }
- it 'returns a number of configured max retries' do
+ it 'returns the number of configured max retries' do
expect(subject.retries_max).to eq 1
end
end
- context 'when max retries value is not defined' do
+ context 'without retries max config option' do
subject { create(:ci_build) }
it 'returns zero' do
expect(subject.retries_max).to eq 0
end
end
+
+ context 'when build is degenerated' do
+ subject { create(:ci_build, :degenerated) }
+
+ it 'returns zero' do
+ expect(subject.retries_max).to eq 0
+ end
+ end
+
+ context 'with integer only config option' do
+ subject { create(:ci_build, options: { retry: 1 }) }
+
+ it 'returns the number of configured max retries' do
+ expect(subject.retries_max).to eq 1
+ end
+ end
+ end
+
+ describe '#retry_when' do
+ context 'with retries when config option' do
+ subject { create(:ci_build, options: { retry: { when: ['some_reason'] } }) }
+
+ it 'returns the configured when' do
+ expect(subject.retry_when).to eq ['some_reason']
+ end
+ end
+
+ context 'without retries when config option' do
+ subject { create(:ci_build) }
+
+ it 'returns always array' do
+ expect(subject.retry_when).to eq ['always']
+ end
+ end
+
+ context 'with integer only config option' do
+ subject { create(:ci_build, options: { retry: 1 }) }
+
+ it 'returns always array' do
+ expect(subject.retry_when).to eq ['always']
+ end
+ end
+ end
+
+ describe '#retry_failure?' do
+ subject { create(:ci_build) }
+
+ context 'when retries max is zero' do
+ before do
+ expect(subject).to receive(:retries_max).at_least(:once).and_return(0)
+ end
+
+ it 'returns false' do
+ expect(subject.retry_failure?).to eq false
+ end
+ end
+
+ context 'when retries max equals retries count' do
+ before do
+ expect(subject).to receive(:retries_max).at_least(:once).and_return(1)
+ expect(subject).to receive(:retries_count).at_least(:once).and_return(1)
+ end
+
+ it 'returns false' do
+ expect(subject.retry_failure?).to eq false
+ end
+ end
+
+ context 'when retries max is higher than retries count' do
+ before do
+ expect(subject).to receive(:retries_max).at_least(:once).and_return(2)
+ expect(subject).to receive(:retries_count).at_least(:once).and_return(1)
+ end
+
+ context 'and retry when is always' do
+ before do
+ expect(subject).to receive(:retry_when).at_least(:once).and_return(['always'])
+ end
+
+ it 'returns true' do
+ expect(subject.retry_failure?).to eq true
+ end
+ end
+
+ context 'and retry when includes the failure_reason' do
+ before do
+ expect(subject).to receive(:failure_reason).at_least(:once).and_return('some_reason')
+ expect(subject).to receive(:retry_when).at_least(:once).and_return(['some_reason'])
+ end
+
+ it 'returns true' do
+ expect(subject.retry_failure?).to eq true
+ end
+ end
+
+ context 'and retry when does not include failure_reason' do
+ before do
+ expect(subject).to receive(:failure_reason).at_least(:once).and_return('some_reason')
+ expect(subject).to receive(:retry_when).at_least(:once).and_return(['some', 'other failure'])
+ end
+
+ it 'returns false' do
+ expect(subject.retry_failure?).to eq false
+ end
+ end
+ end
end
end
@@ -1523,11 +1708,11 @@ describe Ci::Build do
end
end
- describe '#other_actions' do
+ describe '#other_manual_actions' do
let(:build) { create(:ci_build, :manual, pipeline: pipeline) }
let!(:other_build) { create(:ci_build, :manual, pipeline: pipeline, name: 'other action') }
- subject { build.other_actions }
+ subject { build.other_manual_actions }
before do
project.add_developer(user)
@@ -1558,6 +1743,48 @@ describe Ci::Build do
end
end
+ describe '#other_scheduled_actions' do
+ let(:build) { create(:ci_build, :scheduled, pipeline: pipeline) }
+
+ subject { build.other_scheduled_actions }
+
+ before do
+ project.add_developer(user)
+ end
+
+ context "when other build's status is success" do
+ let!(:other_build) { create(:ci_build, :schedulable, :success, pipeline: pipeline, name: 'other action') }
+
+ it 'returns other actions' do
+ is_expected.to contain_exactly(other_build)
+ end
+ end
+
+ context "when other build's status is failed" do
+ let!(:other_build) { create(:ci_build, :schedulable, :failed, pipeline: pipeline, name: 'other action') }
+
+ it 'returns other actions' do
+ is_expected.to contain_exactly(other_build)
+ end
+ end
+
+ context "when other build's status is running" do
+ let!(:other_build) { create(:ci_build, :schedulable, :running, pipeline: pipeline, name: 'other action') }
+
+ it 'does not return other actions' do
+ is_expected.to be_empty
+ end
+ end
+
+ context "when other build's status is scheduled" do
+ let!(:other_build) { create(:ci_build, :scheduled, pipeline: pipeline, name: 'other action') }
+
+ it 'does not return other actions' do
+ is_expected.to contain_exactly(other_build)
+ end
+ end
+ end
+
describe '#persisted_environment' do
let!(:environment) do
create(:environment, project: project, name: "foo-#{project.default_branch}")
@@ -1629,6 +1856,12 @@ describe Ci::Build do
it { is_expected.to be_playable }
end
+
+ context 'when build is a manual and degenerated' do
+ subject { build_stubbed(:ci_build, :manual, :degenerated, status: :manual) }
+
+ it { is_expected.not_to be_playable }
+ end
end
context 'when build is scheduled' do
@@ -1880,6 +2113,7 @@ describe Ci::Build do
{ key: 'CI_COMMIT_BEFORE_SHA', value: build.before_sha, public: true },
{ key: 'CI_COMMIT_REF_NAME', value: build.ref, public: true },
{ key: 'CI_COMMIT_REF_SLUG', value: build.ref_slug, public: true },
+ { key: 'CI_NODE_TOTAL', value: '1', public: true },
{ key: 'CI_BUILD_REF', value: build.sha, public: true },
{ key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true },
{ key: 'CI_BUILD_REF_NAME', value: build.ref, public: true },
@@ -2341,6 +2575,29 @@ describe Ci::Build do
end
end
+ context 'when build is parallelized' do
+ let(:total) { 5 }
+ let(:index) { 3 }
+
+ before do
+ build.options[:parallel] = total
+ build.options[:instance] = index
+ build.name = "#{build.name} #{index}/#{total}"
+ end
+
+ it 'includes CI_NODE_INDEX' do
+ is_expected.to include(
+ { key: 'CI_NODE_INDEX', value: index.to_s, public: true }
+ )
+ end
+
+ it 'includes correct CI_NODE_TOTAL' do
+ is_expected.to include(
+ { key: 'CI_NODE_TOTAL', value: total.to_s, public: true }
+ )
+ end
+ end
+
describe 'variables ordering' do
context 'when variables hierarchy is stubbed' do
let(:build_pre_var) { { key: 'build', value: 'value', public: true } }
@@ -2728,7 +2985,7 @@ describe Ci::Build do
end
context 'when build is configured to be retried' do
- subject { create(:ci_build, :running, options: { retry: 3 }, project: project, user: user) }
+ subject { create(:ci_build, :running, options: { retry: { max: 3 } }, project: project, user: user) }
it 'retries build and assigns the same user to it' do
expect(described_class).to receive(:retry)
@@ -3157,10 +3414,14 @@ describe Ci::Build do
end
describe '#deployment_status' do
+ before do
+ allow_any_instance_of(described_class).to receive(:create_deployment)
+ end
+
context 'when build is a last deployment' do
let(:build) { create(:ci_build, :success, environment: 'production') }
let(:environment) { create(:environment, name: 'production', project: build.project) }
- let!(:deployment) { create(:deployment, environment: environment, project: environment.project, deployable: build) }
+ let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) }
it { expect(build.deployment_status).to eq(:last) }
end
@@ -3168,8 +3429,8 @@ describe Ci::Build do
context 'when there is a newer build with deployment' do
let(:build) { create(:ci_build, :success, environment: 'production') }
let(:environment) { create(:environment, name: 'production', project: build.project) }
- let!(:deployment) { create(:deployment, environment: environment, project: environment.project, deployable: build) }
- let!(:last_deployment) { create(:deployment, environment: environment, project: environment.project) }
+ let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) }
+ let!(:last_deployment) { create(:deployment, :success, environment: environment, project: environment.project) }
it { expect(build.deployment_status).to eq(:out_of_date) }
end
@@ -3177,7 +3438,7 @@ describe Ci::Build do
context 'when build with deployment has failed' do
let(:build) { create(:ci_build, :failed, environment: 'production') }
let(:environment) { create(:environment, name: 'production', project: build.project) }
- let!(:deployment) { create(:deployment, environment: environment, project: environment.project, deployable: build) }
+ let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) }
it { expect(build.deployment_status).to eq(:failed) }
end
@@ -3185,16 +3446,59 @@ describe Ci::Build do
context 'when build with deployment is running' do
let(:build) { create(:ci_build, environment: 'production') }
let(:environment) { create(:environment, name: 'production', project: build.project) }
- let!(:deployment) { create(:deployment, environment: environment, project: environment.project, deployable: build) }
+ let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) }
it { expect(build.deployment_status).to eq(:creating) }
end
+ end
- context 'when build is successful but deployment is not ready yet' do
- let(:build) { create(:ci_build, :success, environment: 'production') }
- let(:environment) { create(:environment, name: 'production', project: build.project) }
+ describe '#degenerated?' do
+ context 'when build is degenerated' do
+ subject { create(:ci_build, :degenerated) }
- it { expect(build.deployment_status).to eq(:creating) }
+ it { is_expected.to be_degenerated }
+ end
+
+ context 'when build is valid' do
+ subject { create(:ci_build) }
+
+ it { is_expected.not_to be_degenerated }
+
+ context 'and becomes degenerated' do
+ before do
+ subject.degenerate!
+ end
+
+ it { is_expected.to be_degenerated }
+ end
+ end
+ end
+
+ describe '#archived?' do
+ context 'when build is degenerated' do
+ subject { create(:ci_build, :degenerated) }
+
+ it { is_expected.to be_archived }
+ end
+
+ context 'for old build' do
+ subject { create(:ci_build, created_at: 1.day.ago) }
+
+ context 'when archive_builds_in is set' do
+ before do
+ stub_application_setting(archive_builds_in_seconds: 3600)
+ end
+
+ it { is_expected.to be_archived }
+ end
+
+ context 'when archive_builds_in is not set' do
+ before do
+ stub_application_setting(archive_builds_in_seconds: nil)
+ end
+
+ it { is_expected.not_to be_archived }
+ end
end
end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 153244b2159..9e6146b8a44 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -1043,6 +1043,11 @@ describe Ci::Pipeline, :mailer do
expect(described_class.newest_first.pluck(:status))
.to eq(%w[skipped failed success canceled])
end
+
+ it 'searches limited backlog' do
+ expect(described_class.newest_first(limit: 1).pluck(:status))
+ .to eq(%w[skipped])
+ end
end
describe '.latest_status' do
@@ -1148,6 +1153,19 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe '.latest_successful_ids_per_project' do
+ let(:projects) { create_list(:project, 2) }
+ let!(:pipeline1) { create(:ci_pipeline, :success, project: projects[0]) }
+ let!(:pipeline2) { create(:ci_pipeline, :success, project: projects[0]) }
+ let!(:pipeline3) { create(:ci_pipeline, :failed, project: projects[0]) }
+ let!(:pipeline4) { create(:ci_pipeline, :success, project: projects[1]) }
+
+ it 'returns expected pipeline ids' do
+ expect(described_class.latest_successful_ids_per_project)
+ .to contain_exactly(pipeline2, pipeline4)
+ end
+ end
+
describe '.internal_sources' do
subject { described_class.internal_sources }
diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb
index 1580ef36127..6b0b23eeab3 100644
--- a/spec/models/clusters/applications/ingress_spec.rb
+++ b/spec/models/clusters/applications/ingress_spec.rb
@@ -5,6 +5,7 @@ describe Clusters::Applications::Ingress do
include_examples 'cluster application core specs', :clusters_applications_ingress
include_examples 'cluster application status specs', :clusters_applications_ingress
+ include_examples 'cluster application helm specs', :clusters_applications_knative
before do
allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
@@ -121,28 +122,5 @@ describe Clusters::Applications::Ingress do
expect(values).to include('stats')
expect(values).to include('podAnnotations')
end
-
- context 'when the helm application does not have a ca_cert' do
- before do
- application.cluster.application_helm.ca_cert = nil
- end
-
- it 'should not include cert files' do
- expect(subject[:'ca.pem']).not_to be_present
- expect(subject[:'cert.pem']).not_to be_present
- expect(subject[:'key.pem']).not_to be_present
- end
- end
-
- it 'should include cert files' do
- expect(subject[:'ca.pem']).to be_present
- expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert)
-
- expect(subject[:'cert.pem']).to be_present
- expect(subject[:'key.pem']).to be_present
-
- cert = OpenSSL::X509::Certificate.new(subject[:'cert.pem'])
- expect(cert.not_after).to be < 60.minutes.from_now
- end
end
end
diff --git a/spec/models/clusters/applications/jupyter_spec.rb b/spec/models/clusters/applications/jupyter_spec.rb
index 9c4396731eb..faaabafddb7 100644
--- a/spec/models/clusters/applications/jupyter_spec.rb
+++ b/spec/models/clusters/applications/jupyter_spec.rb
@@ -2,6 +2,7 @@ require 'rails_helper'
describe Clusters::Applications::Jupyter do
include_examples 'cluster application core specs', :clusters_applications_jupyter
+ include_examples 'cluster application helm specs', :clusters_applications_knative
it { is_expected.to belong_to(:oauth_application) }
@@ -79,29 +80,6 @@ describe Clusters::Applications::Jupyter do
subject { application.files }
- it 'should include cert files' do
- expect(subject[:'ca.pem']).to be_present
- expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert)
-
- expect(subject[:'cert.pem']).to be_present
- expect(subject[:'key.pem']).to be_present
-
- cert = OpenSSL::X509::Certificate.new(subject[:'cert.pem'])
- expect(cert.not_after).to be < 60.minutes.from_now
- end
-
- context 'when the helm application does not have a ca_cert' do
- before do
- application.cluster.application_helm.ca_cert = nil
- end
-
- it 'should not include cert files' do
- expect(subject[:'ca.pem']).not_to be_present
- expect(subject[:'cert.pem']).not_to be_present
- expect(subject[:'key.pem']).not_to be_present
- end
- end
-
it 'should include valid values' do
expect(values).to include('ingress')
expect(values).to include('hub')
diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb
new file mode 100644
index 00000000000..be2a91d566b
--- /dev/null
+++ b/spec/models/clusters/applications/knative_spec.rb
@@ -0,0 +1,77 @@
+require 'rails_helper'
+
+describe Clusters::Applications::Knative do
+ let(:knative) { create(:clusters_applications_knative) }
+
+ include_examples 'cluster application core specs', :clusters_applications_knative
+ include_examples 'cluster application status specs', :clusters_applications_knative
+ include_examples 'cluster application helm specs', :clusters_applications_knative
+
+ describe '.installed' do
+ subject { described_class.installed }
+
+ let!(:cluster) { create(:clusters_applications_knative, :installed) }
+
+ before do
+ create(:clusters_applications_knative, :errored)
+ end
+
+ it { is_expected.to contain_exactly(cluster) }
+ end
+
+ describe '#make_installing!' do
+ before do
+ application.make_installing!
+ end
+
+ context 'application install previously errored with older version' do
+ let(:application) { create(:clusters_applications_knative, :scheduled, version: '0.1.3') }
+
+ it 'updates the application version' do
+ expect(application.reload.version).to eq('0.1.3')
+ end
+ end
+ end
+
+ describe '#make_installed' do
+ subject { described_class.installed }
+
+ let!(:cluster) { create(:clusters_applications_knative, :installed) }
+
+ before do
+ create(:clusters_applications_knative, :errored)
+ end
+
+ it { is_expected.to contain_exactly(cluster) }
+ end
+
+ describe '#install_command' do
+ subject { knative.install_command }
+
+ it 'should be an instance of Helm::InstallCommand' do
+ expect(subject).to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand)
+ end
+
+ it 'should be initialized with knative arguments' do
+ expect(subject.name).to eq('knative')
+ expect(subject.chart).to eq('knative/knative')
+ expect(subject.version).to eq('0.1.3')
+ expect(subject.files).to eq(knative.files)
+ end
+ end
+
+ describe '#files' do
+ let(:application) { knative }
+ let(:values) { subject[:'values.yaml'] }
+
+ subject { application.files }
+
+ it 'should include knative specific keys in the values.yaml file' do
+ expect(values).to include('domain')
+ end
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:hostname) }
+ end
+end
diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb
index f9776acd4c8..86de9dc60f2 100644
--- a/spec/models/clusters/applications/prometheus_spec.rb
+++ b/spec/models/clusters/applications/prometheus_spec.rb
@@ -5,6 +5,7 @@ describe Clusters::Applications::Prometheus do
include_examples 'cluster application core specs', :clusters_applications_prometheus
include_examples 'cluster application status specs', :clusters_applications_prometheus
+ include_examples 'cluster application helm specs', :clusters_applications_knative
describe '.installed' do
subject { described_class.installed }
@@ -109,14 +110,20 @@ describe Clusters::Applications::Prometheus do
end
context 'cluster has kubeclient' do
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:kubernetes_url) { subject.cluster.platform_kubernetes.api_url }
let(:kube_client) { subject.cluster.kubeclient.core_client }
- subject { create(:clusters_applications_prometheus) }
+ subject { create(:clusters_applications_prometheus, cluster: cluster) }
before do
subject.cluster.platform_kubernetes.namespace = 'a-namespace'
- stub_kubeclient_discover(subject.cluster.platform_kubernetes.api_url)
+ stub_kubeclient_discover(cluster.platform_kubernetes.api_url)
+
+ create(:cluster_kubernetes_namespace,
+ cluster: cluster,
+ cluster_project: cluster.cluster_project,
+ project: cluster.cluster_project.project)
end
it 'creates proxy prometheus rest client' do
@@ -181,29 +188,6 @@ describe Clusters::Applications::Prometheus do
subject { application.files }
- it 'should include cert files' do
- expect(subject[:'ca.pem']).to be_present
- expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert)
-
- expect(subject[:'cert.pem']).to be_present
- expect(subject[:'key.pem']).to be_present
-
- cert = OpenSSL::X509::Certificate.new(subject[:'cert.pem'])
- expect(cert.not_after).to be < 60.minutes.from_now
- end
-
- context 'when the helm application does not have a ca_cert' do
- before do
- application.cluster.application_helm.ca_cert = nil
- end
-
- it 'should not include cert files' do
- expect(subject[:'ca.pem']).not_to be_present
- expect(subject[:'cert.pem']).not_to be_present
- expect(subject[:'key.pem']).not_to be_present
- end
- end
-
it 'should include prometheus valid values' do
expect(values).to include('alertmanager')
expect(values).to include('kubeStateMetrics')
diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb
index d5fb1a9d010..052cfdbc4b1 100644
--- a/spec/models/clusters/applications/runner_spec.rb
+++ b/spec/models/clusters/applications/runner_spec.rb
@@ -5,6 +5,7 @@ describe Clusters::Applications::Runner do
include_examples 'cluster application core specs', :clusters_applications_runner
include_examples 'cluster application status specs', :clusters_applications_runner
+ include_examples 'cluster application helm specs', :clusters_applications_knative
it { is_expected.to belong_to(:runner) }
@@ -74,29 +75,6 @@ describe Clusters::Applications::Runner do
subject { application.files }
- it 'should include cert files' do
- expect(subject[:'ca.pem']).to be_present
- expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert)
-
- expect(subject[:'cert.pem']).to be_present
- expect(subject[:'key.pem']).to be_present
-
- cert = OpenSSL::X509::Certificate.new(subject[:'cert.pem'])
- expect(cert.not_after).to be < 60.minutes.from_now
- end
-
- context 'when the helm application does not have a ca_cert' do
- before do
- application.cluster.application_helm.ca_cert = nil
- end
-
- it 'should not include cert files' do
- expect(subject[:'ca.pem']).not_to be_present
- expect(subject[:'cert.pem']).not_to be_present
- expect(subject[:'key.pem']).not_to be_present
- end
- end
-
it 'should include runner valid values' do
expect(values).to include('concurrent')
expect(values).to include('checkInterval')
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index c245e8df815..98d7e799d67 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -16,6 +16,7 @@ describe Clusters::Cluster do
it { is_expected.to have_one(:application_runner) }
it { is_expected.to have_many(:kubernetes_namespaces) }
it { is_expected.to have_one(:kubernetes_namespace) }
+ it { is_expected.to have_one(:cluster_project) }
it { is_expected.to delegate_method(:status).to(:provider) }
it { is_expected.to delegate_method(:status_reason).to(:provider) }
@@ -313,9 +314,10 @@ describe Clusters::Cluster do
let!(:prometheus) { create(:clusters_applications_prometheus, cluster: cluster) }
let!(:runner) { create(:clusters_applications_runner, cluster: cluster) }
let!(:jupyter) { create(:clusters_applications_jupyter, cluster: cluster) }
+ let!(:knative) { create(:clusters_applications_knative, cluster: cluster) }
it 'returns a list of created applications' do
- is_expected.to contain_exactly(helm, ingress, prometheus, runner, jupyter)
+ is_expected.to contain_exactly(helm, ingress, prometheus, runner, jupyter, knative)
end
end
end
@@ -341,4 +343,26 @@ describe Clusters::Cluster do
it { is_expected.to eq(false) }
end
end
+
+ describe '#allow_user_defined_namespace?' do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
+
+ subject { cluster.allow_user_defined_namespace? }
+
+ context 'project type cluster' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'group type cluster' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, :group) }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'instance type cluster' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, :instance) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
end
diff --git a/spec/models/clusters/kubernetes_namespace_spec.rb b/spec/models/clusters/kubernetes_namespace_spec.rb
index dea58fa26c7..c068c4d7739 100644
--- a/spec/models/clusters/kubernetes_namespace_spec.rb
+++ b/spec/models/clusters/kubernetes_namespace_spec.rb
@@ -8,25 +8,33 @@ RSpec.describe Clusters::KubernetesNamespace, type: :model do
it { is_expected.to belong_to(:cluster) }
it { is_expected.to have_one(:platform_kubernetes) }
- describe 'namespace uniqueness validation' do
- let(:cluster_project) { create(:cluster_project) }
+ describe 'has_service_account_token' do
+ subject { described_class.has_service_account_token }
- let(:kubernetes_namespace) do
- build(:cluster_kubernetes_namespace,
- cluster: cluster_project.cluster,
- project: cluster_project.project,
- cluster_project: cluster_project)
+ context 'namespace has service_account_token' do
+ let!(:namespace) { create(:cluster_kubernetes_namespace, :with_token) }
+
+ it { is_expected.to include(namespace) }
end
+ context 'namespace has no service_account_token' do
+ let!(:namespace) { create(:cluster_kubernetes_namespace) }
+
+ it { is_expected.not_to include(namespace) }
+ end
+ end
+
+ describe 'namespace uniqueness validation' do
+ let(:cluster_project) { create(:cluster_project) }
+ let(:kubernetes_namespace) { build(:cluster_kubernetes_namespace, namespace: 'my-namespace') }
+
subject { kubernetes_namespace }
context 'when cluster is using the namespace' do
before do
create(:cluster_kubernetes_namespace,
- cluster: cluster_project.cluster,
- project: cluster_project.project,
- cluster_project: cluster_project,
- namespace: kubernetes_namespace.namespace)
+ cluster: kubernetes_namespace.cluster,
+ namespace: 'my-namespace')
end
it { is_expected.not_to be_valid }
@@ -37,48 +45,79 @@ RSpec.describe Clusters::KubernetesNamespace, type: :model do
end
end
- describe '#set_namespace_and_service_account_to_default' do
- let(:cluster) { platform.cluster }
- let(:cluster_project) { create(:cluster_project, cluster: cluster) }
- let(:kubernetes_namespace) do
- create(:cluster_kubernetes_namespace,
- cluster: cluster_project.cluster,
- project: cluster_project.project,
- cluster_project: cluster_project)
- end
+ describe '#configure_predefined_variables' do
+ let(:kubernetes_namespace) { build(:cluster_kubernetes_namespace) }
+ let(:cluster) { kubernetes_namespace.cluster }
+ let(:platform) { kubernetes_namespace.platform_kubernetes }
- describe 'namespace' do
- let(:platform) { create(:cluster_platform_kubernetes, namespace: namespace) }
+ subject { kubernetes_namespace.configure_predefined_credentials }
- subject { kubernetes_namespace.namespace }
+ describe 'namespace' do
+ before do
+ platform.update_column(:namespace, namespace)
+ end
context 'when platform has a namespace assigned' do
let(:namespace) { 'platform-namespace' }
it 'should copy the namespace' do
- is_expected.to eq('platform-namespace')
+ subject
+
+ expect(kubernetes_namespace.namespace).to eq('platform-namespace')
end
end
context 'when platform does not have namespace assigned' do
+ let(:project) { kubernetes_namespace.project }
let(:namespace) { nil }
+ let(:project_slug) { "#{project.path}-#{project.id}" }
- it 'should set default namespace' do
- project_slug = "#{cluster_project.project.path}-#{cluster_project.project_id}"
+ it 'should fallback to project namespace' do
+ subject
- is_expected.to eq(project_slug)
+ expect(kubernetes_namespace.namespace).to eq(project_slug)
end
end
end
describe 'service_account_name' do
- let(:platform) { create(:cluster_platform_kubernetes) }
-
- subject { kubernetes_namespace.service_account_name }
+ let(:service_account_name) { "#{kubernetes_namespace.namespace}-service-account" }
it 'should set a service account name based on namespace' do
- is_expected.to eq("#{kubernetes_namespace.namespace}-service-account")
+ subject
+
+ expect(kubernetes_namespace.service_account_name).to eq(service_account_name)
end
end
end
+
+ describe '#predefined_variables' do
+ let(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, cluster: cluster, service_account_token: token) }
+ let(:cluster) { create(:cluster, :project, platform_kubernetes: platform) }
+ let(:platform) { create(:cluster_platform_kubernetes, api_url: api_url, ca_cert: ca_pem, token: token) }
+
+ let(:api_url) { 'https://kube.domain.com' }
+ let(:ca_pem) { 'CA PEM DATA' }
+ let(:token) { 'token' }
+
+ let(:kubeconfig) do
+ config_file = expand_fixture_path('config/kubeconfig.yml')
+ config = YAML.safe_load(File.read(config_file))
+ config.dig('users', 0, 'user')['token'] = token
+ config.dig('contexts', 0, 'context')['namespace'] = kubernetes_namespace.namespace
+ config.dig('clusters', 0, 'cluster')['certificate-authority-data'] =
+ Base64.strict_encode64(ca_pem)
+
+ YAML.dump(config)
+ end
+
+ it 'sets the variables' do
+ expect(kubernetes_namespace.predefined_variables).to include(
+ { key: 'KUBE_SERVICE_ACCOUNT', value: kubernetes_namespace.service_account_name, public: true },
+ { key: 'KUBE_NAMESPACE', value: kubernetes_namespace.namespace, public: true },
+ { key: 'KUBE_TOKEN', value: kubernetes_namespace.service_account_token, public: false },
+ { key: 'KUBECONFIG', value: kubeconfig, public: false, file: true }
+ )
+ end
+ end
end
diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb
index e13eb554add..99fd6ccc4d8 100644
--- a/spec/models/clusters/platforms/kubernetes_spec.rb
+++ b/spec/models/clusters/platforms/kubernetes_spec.rb
@@ -58,6 +58,18 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
it { is_expected.to be_truthy }
end
+
+ context 'for group cluster' do
+ let(:namespace) { 'namespace-123' }
+ let(:cluster) { build(:cluster, :group, :provided_by_user) }
+ let(:kubernetes) { cluster.platform_kubernetes }
+
+ before do
+ kubernetes.namespace = namespace
+ end
+
+ it { is_expected.to be_falsey }
+ end
end
context 'when validates api_url' do
@@ -124,9 +136,17 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
end
describe '#kubeclient' do
+ let(:cluster) { create(:cluster, :project) }
+ let(:kubernetes) { build(:cluster_platform_kubernetes, :configured, namespace: 'a-namespace', cluster: cluster) }
+
subject { kubernetes.kubeclient }
- let(:kubernetes) { build(:cluster_platform_kubernetes, :configured, namespace: 'a-namespace') }
+ before do
+ create(:cluster_kubernetes_namespace,
+ cluster: kubernetes.cluster,
+ cluster_project: kubernetes.cluster.cluster_project,
+ project: kubernetes.cluster.cluster_project.project)
+ end
it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::KubeClient) }
end
@@ -186,35 +206,46 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
describe '#predefined_variables' do
let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) }
- let(:kubernetes) { create(:cluster_platform_kubernetes, api_url: api_url, ca_cert: ca_pem, token: token) }
+ let(:kubernetes) { create(:cluster_platform_kubernetes, api_url: api_url, ca_cert: ca_pem) }
let(:api_url) { 'https://kube.domain.com' }
let(:ca_pem) { 'CA PEM DATA' }
- let(:token) { 'token' }
- let(:kubeconfig) do
- config_file = expand_fixture_path('config/kubeconfig.yml')
- config = YAML.load(File.read(config_file))
- config.dig('users', 0, 'user')['token'] = token
- config.dig('contexts', 0, 'context')['namespace'] = namespace
- config.dig('clusters', 0, 'cluster')['certificate-authority-data'] =
- Base64.strict_encode64(ca_pem)
-
- YAML.dump(config)
- end
+ subject { kubernetes.predefined_variables(project: cluster.project) }
shared_examples 'setting variables' do
it 'sets the variables' do
- expect(kubernetes.predefined_variables).to include(
+ expect(subject).to include(
{ key: 'KUBE_URL', value: api_url, public: true },
- { key: 'KUBE_TOKEN', value: token, public: false },
- { key: 'KUBE_NAMESPACE', value: namespace, public: true },
- { key: 'KUBECONFIG', value: kubeconfig, public: false, file: true },
{ key: 'KUBE_CA_PEM', value: ca_pem, public: true },
{ key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true }
)
end
end
+ context 'kubernetes namespace is created with no service account token' do
+ let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, cluster: cluster) }
+
+ it_behaves_like 'setting variables'
+
+ it 'sets KUBE_TOKEN' do
+ expect(subject).to include(
+ { key: 'KUBE_TOKEN', value: kubernetes.token, public: false }
+ )
+ end
+ end
+
+ context 'kubernetes namespace is created with no service account token' do
+ let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, :with_token, cluster: cluster) }
+
+ it_behaves_like 'setting variables'
+
+ it 'sets KUBE_TOKEN' do
+ expect(subject).to include(
+ { key: 'KUBE_TOKEN', value: kubernetes_namespace.service_account_token, public: false }
+ )
+ end
+ end
+
context 'namespace is provided' do
let(:namespace) { 'my-project' }
@@ -223,6 +254,12 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
end
it_behaves_like 'setting variables'
+
+ it 'sets KUBE_TOKEN' do
+ expect(subject).to include(
+ { key: 'KUBE_TOKEN', value: kubernetes.token, public: false }
+ )
+ end
end
context 'no namespace provided' do
@@ -230,11 +267,10 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
it_behaves_like 'setting variables'
- it 'sets the KUBE_NAMESPACE' do
- kube_namespace = kubernetes.predefined_variables.find { |h| h[:key] == 'KUBE_NAMESPACE' }
-
- expect(kube_namespace).not_to be_nil
- expect(kube_namespace[:value]).to match(/\A#{Gitlab::PathRegex::PATH_REGEX_STR}-\d+\z/)
+ it 'sets KUBE_TOKEN' do
+ expect(subject).to include(
+ { key: 'KUBE_TOKEN', value: kubernetes.token, public: false }
+ )
end
end
end
@@ -319,4 +355,27 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
it { is_expected.to include(pods: []) }
end
end
+
+ describe '#update_kubernetes_namespace' do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
+ let(:platform) { cluster.platform }
+
+ context 'when namespace is updated' do
+ it 'should call ConfigureWorker' do
+ expect(ClusterPlatformConfigureWorker).to receive(:perform_async).with(cluster.id).once
+
+ platform.namespace = 'new-namespace'
+ platform.save
+ end
+ end
+
+ context 'when namespace is not updated' do
+ it 'should not call ConfigureWorker' do
+ expect(ClusterPlatformConfigureWorker).not_to receive(:perform_async)
+
+ platform.username = "new-username"
+ platform.save
+ end
+ end
+ end
end
diff --git a/spec/models/compare_spec.rb b/spec/models/compare_spec.rb
index 8e88bb81162..0bc3ee014e6 100644
--- a/spec/models/compare_spec.rb
+++ b/spec/models/compare_spec.rb
@@ -92,4 +92,33 @@ describe Compare do
expect(subject.diff_refs.head_sha).to eq(head_commit.id)
end
end
+
+ describe '#modified_paths' do
+ context 'changes are present' do
+ let(:raw_compare) do
+ Gitlab::Git::Compare.new(
+ project.repository.raw_repository, 'before-create-delete-modify-move', 'after-create-delete-modify-move'
+ )
+ end
+
+ it 'returns affected file paths, without duplication' do
+ expect(subject.modified_paths).to contain_exactly(*%w{
+ foo/for_move.txt
+ foo/bar/for_move.txt
+ foo/for_create.txt
+ foo/for_delete.txt
+ foo/for_edit.txt
+ })
+ end
+ end
+
+ context 'changes are absent' do
+ let(:start_commit) { sample_commit }
+ let(:head_commit) { sample_commit }
+
+ it 'returns empty array' do
+ expect(subject.modified_paths).to eq([])
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb
index 69083bdc125..5713106418d 100644
--- a/spec/models/concerns/awardable_spec.rb
+++ b/spec/models/concerns/awardable_spec.rb
@@ -24,13 +24,29 @@ describe Awardable do
end
end
- describe ".awarded" do
+ describe "#awarded" do
it "filters by user and emoji name" do
expect(Issue.awarded(award_emoji.user, "thumbsup")).to be_empty
expect(Issue.awarded(award_emoji.user, "thumbsdown")).to eq [issue]
expect(Issue.awarded(award_emoji2.user, "thumbsup")).to eq [issue2]
expect(Issue.awarded(award_emoji2.user, "thumbsdown")).to be_empty
end
+
+ it "filters by user and any emoji" do
+ issue3 = create(:issue)
+ create(:award_emoji, awardable: issue3, name: "star", user: award_emoji.user)
+ create(:award_emoji, awardable: issue3, name: "star", user: award_emoji2.user)
+
+ expect(Issue.awarded(award_emoji.user)).to contain_exactly(issue, issue3)
+ expect(Issue.awarded(award_emoji2.user)).to contain_exactly(issue2, issue3)
+ end
+ end
+
+ describe "#not_awarded" do
+ it "returns issues not awarded by user" do
+ expect(Issue.not_awarded(award_emoji.user)).to eq [issue2]
+ expect(Issue.not_awarded(award_emoji2.user)).to eq [issue]
+ end
end
end
diff --git a/spec/models/concerns/blob_language_from_git_attributes_spec.rb b/spec/models/concerns/blob_language_from_git_attributes_spec.rb
new file mode 100644
index 00000000000..7f05073b08e
--- /dev/null
+++ b/spec/models/concerns/blob_language_from_git_attributes_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe BlobLanguageFromGitAttributes do
+ include FakeBlobHelpers
+
+ let(:project) { build(:project, :repository) }
+
+ describe '#language_from_gitattributes' do
+ subject(:blob) { fake_blob(path: 'file.md') }
+
+ it 'returns return value from gitattribute' do
+ expect(blob.project.repository).to receive(:gitattribute).with(blob.path, 'gitlab-language').and_return('erb?parent=json')
+
+ expect(blob.language_from_gitattributes).to eq('erb?parent=json')
+ end
+
+ it 'returns nil if project is absent' do
+ allow(blob).to receive(:project).and_return(nil)
+
+ expect(blob.language_from_gitattributes).to eq(nil)
+ end
+ end
+end
diff --git a/spec/models/concerns/cacheable_attributes_spec.rb b/spec/models/concerns/cacheable_attributes_spec.rb
index f8c2e29fadd..827fbc9d7d5 100644
--- a/spec/models/concerns/cacheable_attributes_spec.rb
+++ b/spec/models/concerns/cacheable_attributes_spec.rb
@@ -41,7 +41,7 @@ describe CacheableAttributes do
expect(minimal_test_class.current_without_cache).to eq(minimal_test_class.last)
end
- it 'can be overriden' do
+ it 'can be overridden' do
minimal_test_class.define_singleton_method(:current_without_cache) do
first
end
@@ -64,7 +64,7 @@ describe CacheableAttributes do
context 'with defaults defined' do
include_context 'with defaults'
- it 'can be overriden' do
+ it 'can be overridden' do
expect(minimal_test_class.defaults).to eq({ foo: 'a', bar: 'b', baz: 'c' })
end
end
diff --git a/spec/models/concerns/deployable_spec.rb b/spec/models/concerns/deployable_spec.rb
new file mode 100644
index 00000000000..6951be903fe
--- /dev/null
+++ b/spec/models/concerns/deployable_spec.rb
@@ -0,0 +1,74 @@
+require 'rails_helper'
+
+describe Deployable do
+ describe '#create_deployment' do
+ let(:deployment) { job.deployment }
+ let(:environment) { deployment&.environment }
+
+ before do
+ job.reload
+ end
+
+ context 'when the deployable object will deploy to production' do
+ let!(:job) { create(:ci_build, :start_review_app) }
+
+ it 'creates a deployment and environment record' do
+ expect(deployment.project).to eq(job.project)
+ expect(deployment.ref).to eq(job.ref)
+ expect(deployment.tag).to eq(job.tag)
+ expect(deployment.sha).to eq(job.sha)
+ expect(deployment.user).to eq(job.user)
+ expect(deployment.deployable).to eq(job)
+ expect(deployment.on_stop).to eq('stop_review_app')
+ expect(environment.name).to eq('review/master')
+ end
+ end
+
+ context 'when the deployable object will stop an environment' do
+ let!(:job) { create(:ci_build, :stop_review_app) }
+
+ it 'does not create a deployment record' do
+ expect(deployment).to be_nil
+ end
+ end
+
+ context 'when the deployable object has already had a deployment' do
+ let!(:job) { create(:ci_build, :start_review_app, deployment: race_deployment) }
+ let!(:race_deployment) { create(:deployment, :success) }
+
+ it 'does not create a new deployment' do
+ expect(deployment).to eq(race_deployment)
+ end
+ end
+
+ context 'when the deployable object will not deploy' do
+ let!(:job) { create(:ci_build) }
+
+ it 'does not create a deployment and environment record' do
+ expect(deployment).to be_nil
+ expect(environment).to be_nil
+ end
+ end
+
+ context 'when environment scope contains invalid character' do
+ let(:job) do
+ create(
+ :ci_build,
+ name: 'job:deploy-to-test-site',
+ environment: '$CI_JOB_NAME',
+ options: {
+ environment: {
+ name: '$CI_JOB_NAME',
+ url: 'http://staging.example.com/$CI_JOB_NAME',
+ on_stop: 'stop_review_app'
+ }
+ })
+ end
+
+ it 'does not create a deployment and environment record' do
+ expect(deployment).to be_nil
+ expect(environment).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/each_batch_spec.rb b/spec/models/concerns/each_batch_spec.rb
index 951690a217b..17224c09693 100644
--- a/spec/models/concerns/each_batch_spec.rb
+++ b/spec/models/concerns/each_batch_spec.rb
@@ -14,40 +14,45 @@ describe EachBatch do
5.times { create(:user, updated_at: 1.day.ago) }
end
- it 'yields an ActiveRecord::Relation when a block is given' do
- model.each_batch do |relation|
- expect(relation).to be_a_kind_of(ActiveRecord::Relation)
+ shared_examples 'each_batch handling' do |kwargs|
+ it 'yields an ActiveRecord::Relation when a block is given' do
+ model.each_batch(kwargs) do |relation|
+ expect(relation).to be_a_kind_of(ActiveRecord::Relation)
+ end
end
- end
- it 'yields a batch index as the second argument' do
- model.each_batch do |_, index|
- expect(index).to eq(1)
+ it 'yields a batch index as the second argument' do
+ model.each_batch(kwargs) do |_, index|
+ expect(index).to eq(1)
+ end
end
- end
- it 'accepts a custom batch size' do
- amount = 0
+ it 'accepts a custom batch size' do
+ amount = 0
- model.each_batch(of: 1) { amount += 1 }
+ model.each_batch(kwargs.merge({ of: 1 })) { amount += 1 }
- expect(amount).to eq(5)
- end
+ expect(amount).to eq(5)
+ end
- it 'does not include ORDER BYs in the yielded relations' do
- model.each_batch do |relation|
- expect(relation.to_sql).not_to include('ORDER BY')
+ it 'does not include ORDER BYs in the yielded relations' do
+ model.each_batch do |relation|
+ expect(relation.to_sql).not_to include('ORDER BY')
+ end
end
- end
- it 'allows updating of the yielded relations' do
- time = Time.now
+ it 'allows updating of the yielded relations' do
+ time = Time.now
- model.each_batch do |relation|
- relation.update_all(updated_at: time)
- end
+ model.each_batch do |relation|
+ relation.update_all(updated_at: time)
+ end
- expect(model.where(updated_at: time).count).to eq(5)
+ expect(model.where(updated_at: time).count).to eq(5)
+ end
end
+
+ it_behaves_like 'each_batch handling', {}
+ it_behaves_like 'each_batch handling', { order_hint: :updated_at }
end
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index ec6374f3963..a4bf3e2350a 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -519,7 +519,7 @@ describe Issuable do
end
end
- context 'substracting time' do
+ context 'subtracting time' do
before do
spend_time(1800)
end
@@ -530,7 +530,7 @@ describe Issuable do
expect(issue.total_time_spent).to eq(900)
end
- context 'when time to substract exceeds the total time spent' do
+ context 'when time to subtract exceeds the total time spent' do
it 'raise a validation error' do
Timecop.travel(1.minute.from_now) do
expect do
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index b8364e0cf88..270b2767c68 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -16,6 +16,22 @@ describe Deployment do
it { is_expected.to validate_presence_of(:ref) }
it { is_expected.to validate_presence_of(:sha) }
+ describe '#scheduled_actions' do
+ subject { deployment.scheduled_actions }
+
+ let(:project) { create(:project, :repository) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, :success, pipeline: pipeline) }
+ let(:deployment) { create(:deployment, deployable: build) }
+
+ it 'delegates to other_scheduled_actions' do
+ expect_any_instance_of(Ci::Build)
+ .to receive(:other_scheduled_actions)
+
+ subject
+ end
+ end
+
describe 'modules' do
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
@@ -26,16 +42,174 @@ describe Deployment do
end
end
- describe 'after_create callbacks' do
- let(:environment) { create(:environment) }
- let(:store) { Gitlab::EtagCaching::Store.new }
+ describe '.success' do
+ subject { described_class.success }
+
+ context 'when deployment status is success' do
+ let(:deployment) { create(:deployment, :success) }
+
+ it { is_expected.to eq([deployment]) }
+ end
+
+ context 'when deployment status is created' do
+ let(:deployment) { create(:deployment, :created) }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'when deployment status is running' do
+ let(:deployment) { create(:deployment, :running) }
+
+ it { is_expected.to be_empty }
+ end
+ end
+
+ describe 'state machine' do
+ context 'when deployment runs' do
+ let(:deployment) { create(:deployment) }
+
+ before do
+ deployment.run!
+ end
+
+ it 'starts running' do
+ Timecop.freeze do
+ expect(deployment).to be_running
+ expect(deployment.finished_at).to be_nil
+ end
+ end
+ end
+
+ context 'when deployment succeeded' do
+ let(:deployment) { create(:deployment, :running) }
+
+ it 'has correct status' do
+ Timecop.freeze do
+ deployment.succeed!
+
+ expect(deployment).to be_success
+ expect(deployment.finished_at).to be_like_time(Time.now)
+ end
+ end
+
+ it 'executes Deployments::SuccessWorker asynchronously' do
+ expect(Deployments::SuccessWorker)
+ .to receive(:perform_async).with(deployment.id)
+
+ deployment.succeed!
+ end
+ end
+
+ context 'when deployment failed' do
+ let(:deployment) { create(:deployment, :running) }
+
+ it 'has correct status' do
+ Timecop.freeze do
+ deployment.drop!
+
+ expect(deployment).to be_failed
+ expect(deployment.finished_at).to be_like_time(Time.now)
+ end
+ end
+ end
+
+ context 'when deployment was canceled' do
+ let(:deployment) { create(:deployment, :running) }
+
+ it 'has correct status' do
+ Timecop.freeze do
+ deployment.cancel!
+
+ expect(deployment).to be_canceled
+ expect(deployment.finished_at).to be_like_time(Time.now)
+ end
+ end
+ end
+ end
+
+ describe '#success?' do
+ subject { deployment.success? }
+
+ context 'when deployment status is success' do
+ let(:deployment) { create(:deployment, :success) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when deployment status is failed' do
+ let(:deployment) { create(:deployment, :failed) }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ describe '#status_name' do
+ subject { deployment.status_name }
+
+ context 'when deployment status is success' do
+ let(:deployment) { create(:deployment, :success) }
+
+ it { is_expected.to eq(:success) }
+ end
+
+ context 'when deployment status is failed' do
+ let(:deployment) { create(:deployment, :failed) }
+
+ it { is_expected.to eq(:failed) }
+ end
+ end
+
+ describe '#finished_at' do
+ subject { deployment.finished_at }
- it 'invalidates the environment etag cache' do
- old_value = store.get(environment.etag_cache_key)
+ context 'when deployment status is created' do
+ let(:deployment) { create(:deployment) }
- create(:deployment, environment: environment)
+ it { is_expected.to be_nil }
+ end
+
+ context 'when deployment status is success' do
+ let(:deployment) { create(:deployment, :success) }
+
+ it { is_expected.to eq(deployment.read_attribute(:finished_at)) }
+ end
- expect(store.get(environment.etag_cache_key)).not_to eq(old_value)
+ context 'when deployment status is success' do
+ let(:deployment) { create(:deployment, :success, finished_at: nil) }
+
+ before do
+ deployment.update_column(:finished_at, nil)
+ end
+
+ it { is_expected.to eq(deployment.read_attribute(:created_at)) }
+ end
+
+ context 'when deployment status is running' do
+ let(:deployment) { create(:deployment, :running) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#deployed_at' do
+ subject { deployment.deployed_at }
+
+ context 'when deployment status is created' do
+ let(:deployment) { create(:deployment) }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when deployment status is success' do
+ let(:deployment) { create(:deployment, :success) }
+
+ it { is_expected.to eq(deployment.read_attribute(:finished_at)) }
+ end
+
+ context 'when deployment status is running' do
+ let(:deployment) { create(:deployment, :running) }
+
+ it { is_expected.to be_nil }
end
end
@@ -96,7 +270,7 @@ describe Deployment do
end
describe '#metrics' do
- let(:deployment) { create(:deployment) }
+ let(:deployment) { create(:deployment, :success) }
let(:prometheus_adapter) { double('prometheus_adapter', can_query?: true) }
subject { deployment.metrics }
@@ -125,7 +299,7 @@ describe Deployment do
describe '#additional_metrics' do
let(:project) { create(:project, :repository) }
- let(:deployment) { create(:deployment, project: project) }
+ let(:deployment) { create(:deployment, :succeed, project: project) }
subject { deployment.additional_metrics }
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 1de95d881a7..9a3f1f1c5a1 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -53,6 +53,25 @@ describe Environment do
end
end
+ describe '.with_deployment' do
+ subject { described_class.with_deployment(sha) }
+
+ let(:environment) { create(:environment) }
+ let(:sha) { RepoHelpers.sample_commit.id }
+
+ context 'when deployment has the specified sha' do
+ let!(:deployment) { create(:deployment, environment: environment, sha: sha) }
+
+ it { is_expected.to eq([environment]) }
+ end
+
+ context 'when deployment does not have the specified sha' do
+ let!(:deployment) { create(:deployment, environment: environment, sha: 'abc') }
+
+ it { is_expected.to be_empty }
+ end
+ end
+
describe '#folder_name' do
context 'when it is inside a folder' do
subject(:environment) do
@@ -95,7 +114,7 @@ describe Environment do
context 'with a last deployment' do
let!(:deployment) do
- create(:deployment, environment: environment, sha: project.commit('master').id)
+ create(:deployment, :success, environment: environment, sha: project.commit('master').id)
end
context 'in the same branch' do
@@ -136,8 +155,8 @@ describe Environment do
describe '#first_deployment_for' do
let(:project) { create(:project, :repository) }
- let!(:deployment) { create(:deployment, environment: environment, ref: commit.parent.id) }
- let!(:deployment1) { create(:deployment, environment: environment, ref: commit.id) }
+ let!(:deployment) { create(:deployment, :succeed, environment: environment, ref: commit.parent.id) }
+ let!(:deployment1) { create(:deployment, :succeed, environment: environment, ref: commit.id) }
let(:head_commit) { project.commit }
let(:commit) { project.commit.parent }
@@ -181,7 +200,8 @@ describe Environment do
let(:build) { create(:ci_build) }
let!(:deployment) do
- create(:deployment, environment: environment,
+ create(:deployment, :success,
+ environment: environment,
deployable: build,
on_stop: 'close_app')
end
@@ -249,7 +269,8 @@ describe Environment do
let(:build) { create(:ci_build, pipeline: pipeline) }
let!(:deployment) do
- create(:deployment, environment: environment,
+ create(:deployment, :success,
+ environment: environment,
deployable: build,
on_stop: 'close_app')
end
@@ -304,7 +325,7 @@ describe Environment do
context 'when last deployment to environment is the most recent one' do
before do
- create(:deployment, environment: environment, ref: 'feature')
+ create(:deployment, :success, environment: environment, ref: 'feature')
end
it { is_expected.to be true }
@@ -312,8 +333,8 @@ describe Environment do
context 'when last deployment to environment is not the most recent' do
before do
- create(:deployment, environment: environment, ref: 'feature')
- create(:deployment, environment: environment, ref: 'master')
+ create(:deployment, :success, environment: environment, ref: 'feature')
+ create(:deployment, :success, environment: environment, ref: 'master')
end
it { is_expected.to be false }
@@ -321,7 +342,7 @@ describe Environment do
end
describe '#actions_for' do
- let(:deployment) { create(:deployment, environment: environment) }
+ let(:deployment) { create(:deployment, :success, environment: environment) }
let(:pipeline) { deployment.deployable.pipeline }
let!(:review_action) { create(:ci_build, :manual, name: 'review-apps', pipeline: pipeline, environment: 'review/$CI_COMMIT_REF_NAME' )}
let!(:production_action) { create(:ci_build, :manual, name: 'production', pipeline: pipeline, environment: 'production' )}
@@ -331,6 +352,70 @@ describe Environment do
end
end
+ describe '.deployments' do
+ subject { environment.deployments }
+
+ context 'when there is a deployment record with created status' do
+ let(:deployment) { create(:deployment, :created, environment: environment) }
+
+ it 'does not return the record' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when there is a deployment record with running status' do
+ let(:deployment) { create(:deployment, :running, environment: environment) }
+
+ it 'does not return the record' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when there is a deployment record with success status' do
+ let(:deployment) { create(:deployment, :success, environment: environment) }
+
+ it 'returns the record' do
+ is_expected.to eq([deployment])
+ end
+ end
+ end
+
+ describe '.last_deployment' do
+ subject { environment.last_deployment }
+
+ before do
+ allow_any_instance_of(Deployment).to receive(:create_ref)
+ end
+
+ context 'when there is an old deployment record' do
+ let!(:previous_deployment) { create(:deployment, :success, environment: environment) }
+
+ context 'when there is a deployment record with created status' do
+ let!(:deployment) { create(:deployment, environment: environment) }
+
+ it 'returns the previous deployment' do
+ is_expected.to eq(previous_deployment)
+ end
+ end
+
+ context 'when there is a deployment record with running status' do
+ let!(:deployment) { create(:deployment, :running, environment: environment) }
+
+ it 'returns the previous deployment' do
+ is_expected.to eq(previous_deployment)
+ end
+ end
+
+ context 'when there is a deployment record with success status' do
+ let!(:deployment) { create(:deployment, :success, environment: environment) }
+
+ it 'returns the latest successful deployment' do
+ is_expected.to eq(deployment)
+ end
+ end
+ end
+ end
+
describe '#has_terminals?' do
subject { environment.has_terminals? }
@@ -338,7 +423,7 @@ describe Environment do
context 'with a deployment service' do
shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
context 'and a deployment' do
- let!(:deployment) { create(:deployment, environment: environment) }
+ let!(:deployment) { create(:deployment, :success, environment: environment) }
it { is_expected.to be_truthy }
end
diff --git a/spec/models/environment_status_spec.rb b/spec/models/environment_status_spec.rb
index e7805d52d75..90f7e4a4590 100644
--- a/spec/models/environment_status_spec.rb
+++ b/spec/models/environment_status_spec.rb
@@ -1,8 +1,10 @@
require 'spec_helper'
describe EnvironmentStatus do
- let(:deployment) { create(:deployment, :review_app) }
- let(:environment) { deployment.environment}
+ include ProjectForksHelper
+
+ let(:deployment) { create(:deployment, :succeed, :review_app) }
+ let(:environment) { deployment.environment }
let(:project) { deployment.project }
let(:merge_request) { create(:merge_request, :deployed_review_app, deployment: deployment) }
let(:sha) { deployment.sha }
@@ -12,7 +14,7 @@ describe EnvironmentStatus do
it { is_expected.to delegate_method(:id).to(:environment) }
it { is_expected.to delegate_method(:name).to(:environment) }
it { is_expected.to delegate_method(:project).to(:environment) }
- it { is_expected.to delegate_method(:deployed_at).to(:deployment).as(:created_at) }
+ it { is_expected.to delegate_method(:deployed_at).to(:deployment) }
it { is_expected.to delegate_method(:status).to(:deployment) }
describe '#project' do
@@ -65,9 +67,9 @@ describe EnvironmentStatus do
let(:admin) { create(:admin) }
let(:pipeline) { create(:ci_pipeline, sha: sha) }
- it 'is based on merge_request.head_pipeline' do
- expect(merge_request).to receive(:head_pipeline).and_return(pipeline)
- expect(merge_request).not_to receive(:merge_pipeline)
+ it 'is based on merge_request.diff_head_sha' do
+ expect(merge_request).to receive(:diff_head_sha)
+ expect(merge_request).not_to receive(:merge_commit_sha)
described_class.for_merge_request(merge_request, admin)
end
@@ -81,11 +83,83 @@ describe EnvironmentStatus do
merge_request.mark_as_merged!
end
- it 'is based on merge_request.merge_pipeline' do
- expect(merge_request).to receive(:merge_pipeline).and_return(pipeline)
- expect(merge_request).not_to receive(:head_pipeline)
+ it 'is based on merge_request.merge_commit_sha' do
+ expect(merge_request).to receive(:merge_commit_sha)
+ expect(merge_request).not_to receive(:diff_head_sha)
described_class.after_merge_request(merge_request, admin)
end
end
+
+ describe '.build_environments_status' do
+ subject { described_class.send(:build_environments_status, merge_request, user, sha) }
+
+ let!(:build) { create(:ci_build, :deploy_to_production, pipeline: pipeline) }
+ let(:environment) { build.deployment.environment }
+ let(:user) { project.owner }
+
+ before do
+ build.deployment&.update!(sha: sha)
+ end
+
+ context 'when environment is created on a forked project' do
+ let(:project) { create(:project, :repository) }
+ let(:forked) { fork_project(project, user, repository: true) }
+ let(:sha) { forked.commit.sha }
+ let(:pipeline) { create(:ci_pipeline, sha: sha, project: forked) }
+
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: forked,
+ target_project: project,
+ target_branch: 'master',
+ head_pipeline: pipeline)
+ end
+
+ it 'returns environment status' do
+ expect(subject.count).to eq(1)
+ expect(subject[0].environment).to eq(environment)
+ expect(subject[0].merge_request).to eq(merge_request)
+ expect(subject[0].sha).to eq(sha)
+ end
+ end
+
+ context 'when environment is created on a target project' do
+ let(:project) { create(:project, :repository) }
+ let(:sha) { project.commit.sha }
+ let(:pipeline) { create(:ci_pipeline, sha: sha, project: project) }
+
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: project,
+ source_branch: 'feature',
+ target_project: project,
+ target_branch: 'master',
+ head_pipeline: pipeline)
+ end
+
+ it 'returns environment status' do
+ expect(subject.count).to eq(1)
+ expect(subject[0].environment).to eq(environment)
+ expect(subject[0].merge_request).to eq(merge_request)
+ expect(subject[0].sha).to eq(sha)
+ end
+
+ context 'when the build stops an environment' do
+ let!(:build) { create(:ci_build, :stop_review_app, pipeline: pipeline) }
+
+ it 'does not return environment status' do
+ expect(subject.count).to eq(0)
+ end
+ end
+
+ context 'when user does not have a permission to see the environment' do
+ let(:user) { create(:user) }
+
+ it 'does not return environment status' do
+ expect(subject.count).to eq(0)
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/lfs_object_spec.rb b/spec/models/lfs_object_spec.rb
index 911f85d7b28..3f929710862 100644
--- a/spec/models/lfs_object_spec.rb
+++ b/spec/models/lfs_object_spec.rb
@@ -8,7 +8,7 @@ describe LfsObject do
expect(subject.local_store?).to eq true
end
- it 'returns false whe file_store is equal to LfsObjectUploader::Store::REMOTE' do
+ it 'returns false when file_store is equal to LfsObjectUploader::Store::REMOTE' do
subject.file_store = LfsObjectUploader::Store::REMOTE
expect(subject.local_store?).to eq false
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index 90cce826b6c..cbe60b3a4a5 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -52,9 +52,9 @@ describe MergeRequestDiff do
context 'when it was not cleaned by the system' do
it 'returns persisted diffs' do
- expect(diff).to receive(:load_diffs)
+ expect(diff).to receive(:load_diffs).and_call_original
- diff.diffs
+ diff.diffs.diff_files
end
end
@@ -76,19 +76,19 @@ describe MergeRequestDiff do
end
it 'returns persisted diffs if cannot compare with diff refs' do
- expect(diff).to receive(:load_diffs)
+ expect(diff).to receive(:load_diffs).and_call_original
diff.update!(head_commit_sha: 'invalid-sha')
- diff.diffs
+ diff.diffs.diff_files
end
it 'returns persisted diffs if diff refs does not exist' do
- expect(diff).to receive(:load_diffs)
+ expect(diff).to receive(:load_diffs).and_call_original
diff.update!(start_commit_sha: nil, base_commit_sha: nil)
- diff.diffs
+ diff.diffs.diff_files
end
end
end
@@ -211,4 +211,38 @@ describe MergeRequestDiff do
expect(diff_with_commits.commits_count).to eq(29)
end
end
+
+ describe '#commits_by_shas' do
+ let(:commit_shas) { diff_with_commits.commit_shas }
+
+ it 'returns empty if no SHAs were provided' do
+ expect(diff_with_commits.commits_by_shas([])).to be_empty
+ end
+
+ it 'returns one SHA' do
+ commits = diff_with_commits.commits_by_shas([commit_shas.first, Gitlab::Git::BLANK_SHA])
+
+ expect(commits.count).to eq(1)
+ end
+
+ it 'returns all matching SHAs' do
+ commits = diff_with_commits.commits_by_shas(commit_shas)
+
+ expect(commits.count).to eq(commit_shas.count)
+ expect(commits.map(&:sha)).to match_array(commit_shas)
+ end
+ end
+
+ describe '#modified_paths' do
+ subject do
+ diff = create(:merge_request_diff)
+ create(:merge_request_diff_file, :new_file, merge_request_diff: diff)
+ create(:merge_request_diff_file, :renamed_file, merge_request_diff: diff)
+ diff
+ end
+
+ it 'returns affected file paths' do
+ expect(subject.modified_paths).to eq(%w{foo bar baz})
+ end
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index c8943f2d86f..131db6a5ff9 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -13,6 +13,20 @@ describe MergeRequest do
it { is_expected.to belong_to(:merge_user).class_name("User") }
it { is_expected.to belong_to(:assignee) }
it { is_expected.to have_many(:merge_request_diffs) }
+
+ context 'for forks' do
+ let!(:project) { create(:project) }
+ let!(:fork) { fork_project(project) }
+ let!(:merge_request) { create(:merge_request, target_project: project, source_project: fork) }
+
+ it 'does not load another project due to inverse relationship' do
+ expect(project.merge_requests.first.target_project.object_id).to eq(project.object_id)
+ end
+
+ it 'finds the associated merge request' do
+ expect(project.merge_requests.find(merge_request.id)).to eq(merge_request)
+ end
+ end
end
describe '#squash_in_progress?' do
@@ -538,9 +552,9 @@ describe MergeRequest do
it 'delegates to the MR diffs' do
merge_request.save
- expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(hash_including(options))
+ expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(hash_including(options)).and_call_original
- merge_request.diffs(options)
+ merge_request.diffs(options).diff_files
end
end
@@ -617,6 +631,44 @@ describe MergeRequest do
end
end
+ describe '#modified_paths' do
+ let(:paths) { double(:paths) }
+ subject(:merge_request) { build(:merge_request) }
+
+ before do
+ expect(diff).to receive(:modified_paths).and_return(paths)
+ end
+
+ context 'when past_merge_request_diff is specified' do
+ let(:another_diff) { double(:merge_request_diff) }
+ let(:diff) { another_diff }
+
+ it 'returns affected file paths from specified past_merge_request_diff' do
+ expect(merge_request.modified_paths(past_merge_request_diff: another_diff)).to eq(paths)
+ end
+ end
+
+ context 'when compare is present' do
+ let(:compare) { double(:compare) }
+ let(:diff) { compare }
+
+ it 'returns affected file paths from compare' do
+ merge_request.compare = compare
+
+ expect(merge_request.modified_paths).to eq(paths)
+ end
+ end
+
+ context 'when no arguments provided' do
+ let(:diff) { merge_request.merge_request_diff }
+ subject(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master') }
+
+ it 'returns affected file paths for merge_request_diff' do
+ expect(merge_request.modified_paths).to eq(paths)
+ end
+ end
+ end
+
describe "#related_notes" do
let!(:merge_request) { create(:merge_request) }
@@ -1822,8 +1874,8 @@ describe MergeRequest do
let(:environments) { create_list(:environment, 3, project: project) }
before do
- create(:deployment, environment: environments.first, ref: 'master', sha: project.commit('master').id)
- create(:deployment, environment: environments.second, ref: 'feature', sha: project.commit('feature').id)
+ create(:deployment, :success, environment: environments.first, ref: 'master', sha: project.commit('master').id)
+ create(:deployment, :success, environment: environments.second, ref: 'feature', sha: project.commit('feature').id)
end
it 'selects deployed environments' do
@@ -1843,7 +1895,7 @@ describe MergeRequest do
let(:source_environment) { create(:environment, project: source_project) }
before do
- create(:deployment, environment: source_environment, ref: 'feature', sha: merge_request.diff_head_sha)
+ create(:deployment, :success, environment: source_environment, ref: 'feature', sha: merge_request.diff_head_sha)
end
it 'selects deployed environments' do
@@ -1854,7 +1906,7 @@ describe MergeRequest do
let(:target_environment) { create(:environment, project: project) }
before do
- create(:deployment, environment: target_environment, tag: true, sha: merge_request.diff_head_sha)
+ create(:deployment, :success, environment: target_environment, tag: true, sha: merge_request.diff_head_sha)
end
it 'selects deployed environments' do
@@ -2597,6 +2649,36 @@ describe MergeRequest do
end
end
+ describe '#includes_any_commits?' do
+ it 'returns false' do
+ expect(subject.includes_any_commits?([])).to be_falsey
+ end
+
+ it 'returns false' do
+ expect(subject.includes_any_commits?([Gitlab::Git::BLANK_SHA])).to be_falsey
+ end
+
+ it 'returns true' do
+ expect(subject.includes_any_commits?([subject.merge_request_diff.head_commit_sha])).to be_truthy
+ end
+
+ it 'returns true even when there is a non-existent comit' do
+ expect(subject.includes_any_commits?([Gitlab::Git::BLANK_SHA, subject.merge_request_diff.head_commit_sha])).to be_truthy
+ end
+
+ context 'unpersisted merge request' do
+ let(:new_mr) { build(:merge_request) }
+
+ it 'returns false' do
+ expect(new_mr.includes_any_commits?([Gitlab::Git::BLANK_SHA])).to be_falsey
+ end
+
+ it 'returns true' do
+ expect(new_mr.includes_any_commits?([subject.merge_request_diff.head_commit_sha])).to be_truthy
+ end
+ end
+ end
+
describe '#can_allow_collaboration?' do
let(:target_project) { create(:project, :public) }
let(:source_project) { fork_project(target_project) }
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 8913644a3ce..2db42fe802a 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -562,6 +562,17 @@ describe Namespace do
it { expect(group.all_projects.to_a).to match_array([project2, project1]) }
end
+ describe '#all_pipelines' do
+ let(:group) { create(:group) }
+ let(:child) { create(:group, parent: group) }
+ let!(:project1) { create(:project_empty_repo, namespace: group) }
+ let!(:project2) { create(:project_empty_repo, namespace: child) }
+ let!(:pipeline1) { create(:ci_empty_pipeline, project: project1) }
+ let!(:pipeline2) { create(:ci_empty_pipeline, project: project2) }
+
+ it { expect(group.all_pipelines.to_a).to match_array([pipeline1, pipeline2]) }
+ end
+
describe '#share_with_group_lock with subgroups', :nested_groups do
context 'when creating a subgroup' do
let(:subgroup) { create(:group, parent: root_group )}
diff --git a/spec/models/postgresql/replication_slot_spec.rb b/spec/models/postgresql/replication_slot_spec.rb
index 919a7526803..e100af7ddc7 100644
--- a/spec/models/postgresql/replication_slot_spec.rb
+++ b/spec/models/postgresql/replication_slot_spec.rb
@@ -3,7 +3,27 @@
require 'spec_helper'
describe Postgresql::ReplicationSlot, :postgresql do
+ describe '.in_use?' do
+ it 'returns true when replication slots are present' do
+ expect(described_class).to receive(:exists?).and_return(true)
+ expect(described_class.in_use?).to be_truthy
+ end
+
+ it 'returns false when replication slots are not present' do
+ expect(described_class.in_use?).to be_falsey
+ end
+
+ it 'returns false if the existence check is invalid' do
+ expect(described_class).to receive(:exists?).and_raise(ActiveRecord::StatementInvalid.new('PG::FeatureNotSupported'))
+ expect(described_class.in_use?).to be_falsey
+ end
+ end
+
describe '.lag_too_great?' do
+ before do
+ expect(described_class).to receive(:in_use?).and_return(true)
+ end
+
it 'returns true when replication lag is too great' do
expect(described_class)
.to receive(:pluck)
diff --git a/spec/models/project_services/discord_service_spec.rb b/spec/models/project_services/discord_service_spec.rb
new file mode 100644
index 00000000000..be82f223478
--- /dev/null
+++ b/spec/models/project_services/discord_service_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+describe DiscordService do
+ it_behaves_like "chat service", "Discord notifications" do
+ let(:client) { Discordrb::Webhooks::Client }
+ let(:client_arguments) { { url: webhook_url } }
+ let(:content_key) { :content }
+ end
+end
diff --git a/spec/models/project_services/hangouts_chat_service_spec.rb b/spec/models/project_services/hangouts_chat_service_spec.rb
index cfa55188a64..0505ac9b49c 100644
--- a/spec/models/project_services/hangouts_chat_service_spec.rb
+++ b/spec/models/project_services/hangouts_chat_service_spec.rb
@@ -1,246 +1,11 @@
-require 'spec_helper'
+# frozen_string_literal: true
-describe HangoutsChatService do
- describe 'Associations' do
- it { is_expected.to belong_to :project }
- it { is_expected.to have_one :service_hook }
- end
-
- describe 'Validations' do
- context 'when service is active' do
- before do
- subject.active = true
- end
-
- it { is_expected.to validate_presence_of(:webhook) }
- it_behaves_like 'issue tracker service URL attribute', :webhook
- end
-
- context 'when service is inactive' do
- before do
- subject.active = false
- end
-
- it { is_expected.not_to validate_presence_of(:webhook) }
- end
- end
-
- describe '#execute' do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
- let(:webhook_url) { 'https://example.gitlab.com/' }
-
- before do
- allow(subject).to receive_messages(
- project: project,
- project_id: project.id,
- service_hook: true,
- webhook: webhook_url
- )
-
- WebMock.stub_request(:post, webhook_url)
- end
-
- shared_examples 'Hangouts Chat service' do
- it 'calls Hangouts Chat API' do
- subject.execute(sample_data)
-
- expect(WebMock)
- .to have_requested(:post, webhook_url)
- .with { |req| req.body =~ /\A{"text":.+}\Z/ }
- .once
- end
- end
-
- context 'with push events' do
- let(:sample_data) do
- Gitlab::DataBuilder::Push.build_sample(project, user)
- end
-
- it_behaves_like 'Hangouts Chat service'
-
- it 'specifies the webhook when it is configured' do
- expect(HangoutsChat::Sender).to receive(:new).with(webhook_url).and_return(double(:hangouts_chat_service).as_null_object)
-
- subject.execute(sample_data)
- end
-
- context 'with not default branch' do
- let(:sample_data) do
- Gitlab::DataBuilder::Push.build(project, user, nil, nil, 'not-the-default-branch')
- end
-
- context 'when notify_only_default_branch enabled' do
- before do
- subject.notify_only_default_branch = true
- end
-
- it 'does not call the Hangouts Chat API' do
- result = subject.execute(sample_data)
-
- expect(result).to be_falsy
- end
- end
-
- context 'when notify_only_default_branch disabled' do
- before do
- subject.notify_only_default_branch = false
- end
-
- it_behaves_like 'Hangouts Chat service'
- end
- end
- end
-
- context 'with issue events' do
- let(:opts) { { title: 'Awesome issue', description: 'please fix' } }
- let(:sample_data) do
- service = Issues::CreateService.new(project, user, opts)
- issue = service.execute
- service.hook_data(issue, 'open')
- end
-
- it_behaves_like 'Hangouts Chat service'
- end
-
- context 'with merge events' do
- let(:opts) do
- {
- title: 'Awesome merge_request',
- description: 'please fix',
- source_branch: 'feature',
- target_branch: 'master'
- }
- end
-
- let(:sample_data) do
- service = MergeRequests::CreateService.new(project, user, opts)
- merge_request = service.execute
- service.hook_data(merge_request, 'open')
- end
-
- before do
- project.add_developer(user)
- end
+require "spec_helper"
- it_behaves_like 'Hangouts Chat service'
- end
-
- context 'with wiki page events' do
- let(:opts) do
- {
- title: 'Awesome wiki_page',
- content: 'Some text describing some thing or another',
- format: 'md',
- message: 'user created page: Awesome wiki_page'
- }
- end
- let(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: opts) }
- let(:sample_data) { Gitlab::DataBuilder::WikiPage.build(wiki_page, user, 'create') }
-
- it_behaves_like 'Hangouts Chat service'
- end
-
- context 'with note events' do
- let(:sample_data) { Gitlab::DataBuilder::Note.build(note, user) }
-
- context 'with commit comment' do
- let(:note) do
- create(:note_on_commit, author: user,
- project: project,
- commit_id: project.repository.commit.id,
- note: 'a comment on a commit')
- end
-
- it_behaves_like 'Hangouts Chat service'
- end
-
- context 'with merge request comment' do
- let(:note) do
- create(:note_on_merge_request, project: project,
- note: 'merge request note')
- end
-
- it_behaves_like 'Hangouts Chat service'
- end
-
- context 'with issue comment' do
- let(:note) do
- create(:note_on_issue, project: project, note: 'issue note')
- end
-
- it_behaves_like 'Hangouts Chat service'
- end
-
- context 'with snippet comment' do
- let(:note) do
- create(:note_on_project_snippet, project: project,
- note: 'snippet note')
- end
-
- it_behaves_like 'Hangouts Chat service'
- end
- end
-
- context 'with pipeline events' do
- let(:pipeline) do
- create(:ci_pipeline,
- project: project, status: status,
- sha: project.commit.sha, ref: project.default_branch)
- end
- let(:sample_data) { Gitlab::DataBuilder::Pipeline.build(pipeline) }
-
- context 'with failed pipeline' do
- let(:status) { 'failed' }
-
- it_behaves_like 'Hangouts Chat service'
- end
-
- context 'with succeeded pipeline' do
- let(:status) { 'success' }
-
- context 'with default notify_only_broken_pipelines' do
- it 'does not call Hangouts Chat API' do
- result = subject.execute(sample_data)
-
- expect(result).to be_falsy
- end
- end
-
- context 'when notify_only_broken_pipelines is false' do
- before do
- subject.notify_only_broken_pipelines = false
- end
-
- it_behaves_like 'Hangouts Chat service'
- end
- end
-
- context 'with not default branch' do
- let(:pipeline) do
- create(:ci_pipeline, project: project, status: 'failed', ref: 'not-the-default-branch')
- end
-
- context 'when notify_only_default_branch enabled' do
- before do
- subject.notify_only_default_branch = true
- end
-
- it 'does not call the Hangouts Chat API' do
- result = subject.execute(sample_data)
-
- expect(result).to be_falsy
- end
- end
-
- context 'when notify_only_default_branch disabled' do
- before do
- subject.notify_only_default_branch = false
- end
-
- it_behaves_like 'Hangouts Chat service'
- end
- end
- end
+describe HangoutsChatService do
+ it_behaves_like "chat service", "Hangouts Chat" do
+ let(:client) { HangoutsChat::Sender }
+ let(:client_arguments) { webhook_url }
+ let(:content_key) { :text }
end
end
diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
index 68ab9fd08ec..9c27357ffaf 100644
--- a/spec/models/project_services/kubernetes_service_spec.rb
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -253,7 +253,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
end
end
- describe '#predefined_variables' do
+ describe '#predefined_variable' do
let(:kubeconfig) do
config_file = expand_fixture_path('config/kubeconfig.yml')
config = YAML.load(File.read(config_file))
@@ -274,7 +274,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
shared_examples 'setting variables' do
it 'sets the variables' do
- expect(subject.predefined_variables).to include(
+ expect(subject.predefined_variables(project: project)).to include(
{ key: 'KUBE_URL', value: 'https://kube.domain.com', public: true },
{ key: 'KUBE_TOKEN', value: 'token', public: false },
{ key: 'KUBE_NAMESPACE', value: namespace, public: true },
@@ -301,7 +301,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
it_behaves_like 'setting variables'
it 'sets the KUBE_NAMESPACE' do
- kube_namespace = subject.predefined_variables.find { |h| h[:key] == 'KUBE_NAMESPACE' }
+ kube_namespace = subject.predefined_variables(project: project).find { |h| h[:key] == 'KUBE_NAMESPACE' }
expect(kube_namespace).not_to be_nil
expect(kube_namespace[:value]).to match(/\A#{Gitlab::PathRegex::PATH_REGEX_STR}-\d+\z/)
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index e66838edd1a..bdff68cee8b 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -8,6 +8,7 @@ describe Project do
it { is_expected.to belong_to(:group) }
it { is_expected.to belong_to(:namespace) }
it { is_expected.to belong_to(:creator).class_name('User') }
+ it { is_expected.to belong_to(:pool_repository) }
it { is_expected.to have_many(:users) }
it { is_expected.to have_many(:services) }
it { is_expected.to have_many(:events) }
@@ -32,6 +33,7 @@ describe Project do
it { is_expected.to have_one(:asana_service) }
it { is_expected.to have_many(:boards) }
it { is_expected.to have_one(:campfire_service) }
+ it { is_expected.to have_one(:discord_service) }
it { is_expected.to have_one(:drone_ci_service) }
it { is_expected.to have_one(:emails_on_push_service) }
it { is_expected.to have_one(:pipelines_email_service) }
@@ -88,6 +90,10 @@ describe Project do
it { is_expected.to have_many(:project_deploy_tokens) }
it { is_expected.to have_many(:deploy_tokens).through(:project_deploy_tokens) }
+ it 'has an inverse relationship with merge requests' do
+ expect(described_class.reflect_on_association(:merge_requests).has_inverse?).to eq(:target_project)
+ end
+
context 'after initialized' do
it "has a project_feature" do
expect(described_class.new.project_feature).to be_present
@@ -2401,12 +2407,24 @@ describe Project do
it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
- context 'when user configured kubernetes from CI/CD > Clusters' do
+ context 'when user configured kubernetes from CI/CD > Clusters and KubernetesNamespace migration has not been executed' do
let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:project) { cluster.project }
it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
+
+ context 'when user configured kubernetes from CI/CD > Clusters and KubernetesNamespace migration has been executed' do
+ let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, :with_token) }
+ let!(:cluster) { kubernetes_namespace.cluster }
+ let(:project) { kubernetes_namespace.project }
+
+ it 'should return token from kubernetes namespace' do
+ expect(project.deployment_variables).to include(
+ { key: 'KUBE_TOKEN', value: kubernetes_namespace.service_account_token, public: false }
+ )
+ end
+ end
end
end
@@ -2746,7 +2764,7 @@ describe Project do
.to raise_error(ActiveRecord::RecordNotSaved, error_message)
end
- it 'updates the project succesfully' do
+ it 'updates the project successfully' do
merge_request = create(:merge_request, target_project: project, source_project: project)
expect { project.append_or_update_attribute(:merge_requests, [merge_request]) }
@@ -3314,7 +3332,7 @@ describe Project do
end
end
- context 'when explicitely enabled' do
+ context 'when explicitly enabled' do
context 'when domain is empty' do
before do
create(:project_auto_devops, project: project, domain: nil)
@@ -3959,6 +3977,62 @@ describe Project do
end
end
+ describe '.deployments' do
+ subject { project.deployments }
+
+ let(:project) { create(:project) }
+
+ before do
+ allow_any_instance_of(Deployment).to receive(:create_ref)
+ end
+
+ context 'when there is a deployment record with created status' do
+ let(:deployment) { create(:deployment, :created, project: project) }
+
+ it 'does not return the record' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when there is a deployment record with running status' do
+ let(:deployment) { create(:deployment, :running, project: project) }
+
+ it 'does not return the record' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when there is a deployment record with success status' do
+ let(:deployment) { create(:deployment, :success, project: project) }
+
+ it 'returns the record' do
+ is_expected.to eq([deployment])
+ end
+ end
+ end
+
+ describe '#snippets_visible?' do
+ it 'returns true when a logged in user can read snippets' do
+ project = create(:project, :public)
+ user = create(:user)
+
+ expect(project.snippets_visible?(user)).to eq(true)
+ end
+
+ it 'returns true when an anonymous user can read snippets' do
+ project = create(:project, :public)
+
+ expect(project.snippets_visible?).to eq(true)
+ end
+
+ it 'returns false when a user can not read snippets' do
+ project = create(:project, :private)
+ user = create(:user)
+
+ expect(project.snippets_visible?(user)).to eq(false)
+ end
+ end
+
def rugged_config
rugged_repo(project.repository).config
end
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index f38fc191943..cc5e34782ec 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -145,7 +145,7 @@ describe ProjectWiki do
end
it "returns nil if the page does not exist" do
- expect(subject.find_page("non-existant")).to eq(nil)
+ expect(subject.find_page("non-existent")).to eq(nil)
end
it "can find a page by slug" do
@@ -226,7 +226,7 @@ describe ProjectWiki do
end
it 'returns nil if the page does not exist' do
- expect(subject.find_file('non-existant')).to eq(nil)
+ expect(subject.find_file('non-existent')).to eq(nil)
end
it 'returns a Gitlab::Git::WikiFile instance' do
diff --git a/spec/models/shard_spec.rb b/spec/models/shard_spec.rb
new file mode 100644
index 00000000000..83104711b55
--- /dev/null
+++ b/spec/models/shard_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literals: true
+require 'spec_helper'
+
+describe Shard do
+ describe '.populate!' do
+ it 'creates shards based on the config file' do
+ expect(described_class.all).to be_empty
+
+ stub_storage_settings(foo: {}, bar: {}, baz: {})
+
+ described_class.populate!
+
+ expect(described_class.all.map(&:name)).to match_array(%w[default foo bar baz])
+ end
+ end
+
+ describe '.by_name' do
+ let(:default_shard) { described_class.find_by(name: 'default') }
+
+ before do
+ described_class.populate!
+ end
+
+ it 'returns an existing shard' do
+ expect(described_class.by_name('default')).to eq(default_shard)
+ end
+
+ it 'creates a new shard' do
+ result = described_class.by_name('foo')
+
+ expect(result).not_to eq(default_shard)
+ expect(result.name).to eq('foo')
+ end
+
+ it 'retries if creation races' do
+ expect(described_class)
+ .to receive(:find_or_create_by)
+ .with(name: 'default')
+ .and_raise(ActiveRecord::RecordNotUnique, 'fail')
+ .once
+
+ expect(described_class)
+ .to receive(:find_or_create_by)
+ .with(name: 'default')
+ .and_call_original
+
+ expect(described_class.by_name('default')).to eq(default_shard)
+ end
+ end
+end
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index e09d89d235d..7a7272ccb60 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -131,6 +131,217 @@ describe Snippet do
end
end
+ describe '.with_optional_visibility' do
+ context 'when a visibility level is provided' do
+ it 'returns snippets with the given visibility' do
+ create(:snippet, :private)
+
+ snippet = create(:snippet, :public)
+ snippets = described_class
+ .with_optional_visibility(Gitlab::VisibilityLevel::PUBLIC)
+
+ expect(snippets).to eq([snippet])
+ end
+ end
+
+ context 'when a visibility level is not provided' do
+ it 'returns all snippets' do
+ snippet1 = create(:snippet, :public)
+ snippet2 = create(:snippet, :private)
+ snippets = described_class.with_optional_visibility
+
+ expect(snippets).to include(snippet1, snippet2)
+ end
+ end
+ end
+
+ describe '.only_global_snippets' do
+ it 'returns snippets not associated with any projects' do
+ create(:project_snippet)
+
+ snippet = create(:snippet)
+ snippets = described_class.only_global_snippets
+
+ expect(snippets).to eq([snippet])
+ end
+ end
+
+ describe '.only_include_projects_visible_to' do
+ let!(:project1) { create(:project, :public) }
+ let!(:project2) { create(:project, :internal) }
+ let!(:project3) { create(:project, :private) }
+ let!(:snippet1) { create(:project_snippet, project: project1) }
+ let!(:snippet2) { create(:project_snippet, project: project2) }
+ let!(:snippet3) { create(:project_snippet, project: project3) }
+
+ context 'when a user is provided' do
+ it 'returns snippets visible to the user' do
+ user = create(:user)
+
+ snippets = described_class.only_include_projects_visible_to(user)
+
+ expect(snippets).to include(snippet1, snippet2)
+ expect(snippets).not_to include(snippet3)
+ end
+ end
+
+ context 'when a user is not provided' do
+ it 'returns snippets visible to anonymous users' do
+ snippets = described_class.only_include_projects_visible_to
+
+ expect(snippets).to include(snippet1)
+ expect(snippets).not_to include(snippet2, snippet3)
+ end
+ end
+ end
+
+ describe 'only_include_projects_with_snippets_enabled' do
+ context 'when the include_private option is enabled' do
+ it 'includes snippets for projects with snippets set to private' do
+ project = create(:project)
+
+ project.project_feature
+ .update(snippets_access_level: ProjectFeature::PRIVATE)
+
+ snippet = create(:project_snippet, project: project)
+
+ snippets = described_class
+ .only_include_projects_with_snippets_enabled(include_private: true)
+
+ expect(snippets).to eq([snippet])
+ end
+ end
+
+ context 'when the include_private option is not enabled' do
+ it 'does not include snippets for projects that have snippets set to private' do
+ project = create(:project)
+
+ project.project_feature
+ .update(snippets_access_level: ProjectFeature::PRIVATE)
+
+ create(:project_snippet, project: project)
+
+ snippets = described_class.only_include_projects_with_snippets_enabled
+
+ expect(snippets).to be_empty
+ end
+ end
+
+ it 'includes snippets for projects with snippets enabled' do
+ project = create(:project)
+
+ project.project_feature
+ .update(snippets_access_level: ProjectFeature::ENABLED)
+
+ snippet = create(:project_snippet, project: project)
+ snippets = described_class.only_include_projects_with_snippets_enabled
+
+ expect(snippets).to eq([snippet])
+ end
+ end
+
+ describe '.only_include_authorized_projects' do
+ it 'only includes snippets for projects the user is authorized to see' do
+ user = create(:user)
+ project1 = create(:project, :private)
+ project2 = create(:project, :private)
+
+ project1.team.add_developer(user)
+
+ create(:project_snippet, project: project2)
+
+ snippet = create(:project_snippet, project: project1)
+ snippets = described_class.only_include_authorized_projects(user)
+
+ expect(snippets).to eq([snippet])
+ end
+ end
+
+ describe '.for_project_with_user' do
+ context 'when a user is provided' do
+ it 'returns an empty collection if the user can not view the snippets' do
+ project = create(:project, :private)
+ user = create(:user)
+
+ project.project_feature
+ .update(snippets_access_level: ProjectFeature::ENABLED)
+
+ create(:project_snippet, :public, project: project)
+
+ expect(described_class.for_project_with_user(project, user)).to be_empty
+ end
+
+ it 'returns the snippets if the user is a member of the project' do
+ project = create(:project, :private)
+ user = create(:user)
+ snippet = create(:project_snippet, project: project)
+
+ project.team.add_developer(user)
+
+ snippets = described_class.for_project_with_user(project, user)
+
+ expect(snippets).to eq([snippet])
+ end
+
+ it 'returns public snippets for a public project the user is not a member of' do
+ project = create(:project, :public)
+
+ project.project_feature
+ .update(snippets_access_level: ProjectFeature::ENABLED)
+
+ user = create(:user)
+ snippet = create(:project_snippet, :public, project: project)
+
+ create(:project_snippet, :private, project: project)
+
+ snippets = described_class.for_project_with_user(project, user)
+
+ expect(snippets).to eq([snippet])
+ end
+ end
+
+ context 'when a user is not provided' do
+ it 'returns an empty collection for a private project' do
+ project = create(:project, :private)
+
+ project.project_feature
+ .update(snippets_access_level: ProjectFeature::ENABLED)
+
+ create(:project_snippet, :public, project: project)
+
+ expect(described_class.for_project_with_user(project)).to be_empty
+ end
+
+ it 'returns public snippets for a public project' do
+ project = create(:project, :public)
+ snippet = create(:project_snippet, :public, project: project)
+
+ project.project_feature
+ .update(snippets_access_level: ProjectFeature::PUBLIC)
+
+ create(:project_snippet, :private, project: project)
+
+ snippets = described_class.for_project_with_user(project)
+
+ expect(snippets).to eq([snippet])
+ end
+ end
+ end
+
+ describe '.visible_to_or_authored_by' do
+ it 'returns snippets visible to the user' do
+ user = create(:user)
+ snippet1 = create(:snippet, :public)
+ snippet2 = create(:snippet, :private, author: user)
+ snippet3 = create(:snippet, :private)
+
+ snippets = described_class.visible_to_or_authored_by(user)
+
+ expect(snippets).to include(snippet1, snippet2)
+ expect(snippets).not_to include(snippet3)
+ end
+ end
+
describe '#participants' do
let(:project) { create(:project, :public) }
let(:snippet) { create(:snippet, content: 'foo', project: project) }
diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb
index 36b8e5d304f..5a0df9fbbb0 100644
--- a/spec/models/upload_spec.rb
+++ b/spec/models/upload_spec.rb
@@ -21,7 +21,8 @@ describe Upload do
path: __FILE__,
size: described_class::CHECKSUM_THRESHOLD + 1.kilobyte,
model: build_stubbed(:user),
- uploader: double('ExampleUploader')
+ uploader: double('ExampleUploader'),
+ store: ObjectStorage::Store::LOCAL
)
expect(UploadChecksumWorker)
@@ -35,7 +36,8 @@ describe Upload do
path: __FILE__,
size: described_class::CHECKSUM_THRESHOLD,
model: build_stubbed(:user),
- uploader: double('ExampleUploader')
+ uploader: double('ExampleUploader'),
+ store: ObjectStorage::Store::LOCAL
)
expect { upload.save }
@@ -60,7 +62,7 @@ describe Upload do
describe '#absolute_path' do
it 'returns the path directly when already absolute' do
path = '/path/to/namespace/project/secret/file.jpg'
- upload = described_class.new(path: path)
+ upload = described_class.new(path: path, store: ObjectStorage::Store::LOCAL)
expect(upload).not_to receive(:uploader_class)
@@ -69,7 +71,7 @@ describe Upload do
it "delegates to the uploader's absolute_path method" do
uploader = spy('FakeUploader')
- upload = described_class.new(path: 'secret/file.jpg')
+ upload = described_class.new(path: 'secret/file.jpg', store: ObjectStorage::Store::LOCAL)
expect(upload).to receive(:uploader_class).and_return(uploader)
upload.absolute_path
@@ -81,7 +83,8 @@ describe Upload do
describe '#calculate_checksum!' do
let(:upload) do
described_class.new(path: __FILE__,
- size: described_class::CHECKSUM_THRESHOLD - 1.megabyte)
+ size: described_class::CHECKSUM_THRESHOLD - 1.megabyte,
+ store: ObjectStorage::Store::LOCAL)
end
it 'sets `checksum` to SHA256 sum of the file' do
@@ -91,7 +94,7 @@ describe Upload do
.to change { upload.checksum }.from(nil).to(expected)
end
- it 'sets `checksum` to nil for a non-existant file' do
+ it 'sets `checksum` to nil for a non-existent file' do
expect(upload).to receive(:exist?).and_return(false)
checksum = Digest::SHA256.file(__FILE__).hexdigest
@@ -104,15 +107,56 @@ describe Upload do
describe '#exist?' do
it 'returns true when the file exists' do
- upload = described_class.new(path: __FILE__)
+ upload = described_class.new(path: __FILE__, store: ObjectStorage::Store::LOCAL)
expect(upload).to exist
end
- it 'returns false when the file does not exist' do
- upload = described_class.new(path: "#{__FILE__}-nope")
+ context 'when the file does not exist' do
+ it 'returns false' do
+ upload = described_class.new(path: "#{__FILE__}-nope", store: ObjectStorage::Store::LOCAL)
- expect(upload).not_to exist
+ expect(upload).not_to exist
+ end
+
+ context 'when the record is persisted' do
+ it 'sends a message to Sentry' do
+ upload = create(:upload, :issuable_upload)
+
+ expect(Gitlab::Sentry).to receive(:enabled?).and_return(true)
+ expect(Raven).to receive(:capture_message).with("Upload file does not exist", extra: upload.attributes)
+
+ upload.exist?
+ end
+
+ it 'increments a metric counter to signal a problem' do
+ upload = create(:upload, :issuable_upload)
+
+ counter = double(:counter)
+ expect(counter).to receive(:increment)
+ expect(Gitlab::Metrics).to receive(:counter).with(:upload_file_does_not_exist_total, 'The number of times an upload record could not find its file').and_return(counter)
+
+ upload.exist?
+ end
+ end
+
+ context 'when the record is not persisted' do
+ it 'does not send a message to Sentry' do
+ upload = described_class.new(path: "#{__FILE__}-nope", store: ObjectStorage::Store::LOCAL)
+
+ expect(Raven).not_to receive(:capture_message)
+
+ upload.exist?
+ end
+
+ it 'does not increment a metric counter' do
+ upload = described_class.new(path: "#{__FILE__}-nope", store: ObjectStorage::Store::LOCAL)
+
+ expect(Gitlab::Metrics).not_to receive(:counter)
+
+ upload.exist?
+ end
+ end
end
end
diff --git a/spec/models/user_preference_spec.rb b/spec/models/user_preference_spec.rb
index 64d9d9a78b4..2898613545c 100644
--- a/spec/models/user_preference_spec.rb
+++ b/spec/models/user_preference_spec.rb
@@ -6,22 +6,43 @@ describe UserPreference do
describe '#set_notes_filter' do
let(:issuable) { build_stubbed(:issue) }
let(:user_preference) { create(:user_preference) }
- let(:only_comments) { described_class::NOTES_FILTERS[:only_comments] }
- it 'returns updated discussion filter' do
- filter_name =
- user_preference.set_notes_filter(only_comments, issuable)
+ shared_examples 'setting system notes' do
+ it 'returns updated discussion filter' do
+ filter_name =
+ user_preference.set_notes_filter(filter, issuable)
+
+ expect(filter_name).to eq(filter)
+ end
+
+ it 'updates discussion filter for issuable class' do
+ user_preference.set_notes_filter(filter, issuable)
+
+ expect(user_preference.reload.issue_notes_filter).to eq(filter)
+ end
+ end
+
+ context 'when filter is set to all notes' do
+ let(:filter) { described_class::NOTES_FILTERS[:all_notes] }
+
+ it_behaves_like 'setting system notes'
+ end
+
+ context 'when filter is set to only comments' do
+ let(:filter) { described_class::NOTES_FILTERS[:only_comments] }
- expect(filter_name).to eq(only_comments)
+ it_behaves_like 'setting system notes'
end
- it 'updates discussion filter for issuable class' do
- user_preference.set_notes_filter(only_comments, issuable)
+ context 'when filter is set to only activity' do
+ let(:filter) { described_class::NOTES_FILTERS[:only_activity] }
- expect(user_preference.reload.issue_notes_filter).to eq(only_comments)
+ it_behaves_like 'setting system notes'
end
context 'when notes_filter parameter is invalid' do
+ let(:only_comments) { described_class::NOTES_FILTERS[:only_comments] }
+
it 'returns the current notes filter' do
user_preference.set_notes_filter(only_comments, issuable)
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 4e7c8523e65..0ac5bd666ae 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -183,6 +183,12 @@ describe User do
expect(found_user.commit_email).to eq(user.email)
end
+ it 'returns the private commit email when commit_email has _private' do
+ user.update_column(:commit_email, Gitlab::PrivateCommitEmail::TOKEN)
+
+ expect(user.commit_email).to eq(user.private_commit_email)
+ end
+
it 'can be set to a confirmed email' do
confirmed = create(:email, :confirmed, user: user)
user.commit_email = confirmed.email
@@ -333,6 +339,40 @@ describe User do
expect(user).to be_valid
end
end
+
+ context 'set_commit_email' do
+ it 'keeps commit email when private commit email is being used' do
+ user = create(:user, commit_email: Gitlab::PrivateCommitEmail::TOKEN)
+
+ expect(user.read_attribute(:commit_email)).to eq(Gitlab::PrivateCommitEmail::TOKEN)
+ end
+
+ it 'keeps the commit email when nil' do
+ user = create(:user, commit_email: nil)
+
+ expect(user.read_attribute(:commit_email)).to be_nil
+ end
+
+ it 'reverts to nil when email is not verified' do
+ user = create(:user, commit_email: "foo@bar.com")
+
+ expect(user.read_attribute(:commit_email)).to be_nil
+ end
+ end
+
+ context 'owns_commit_email' do
+ it 'accepts private commit email' do
+ user = build(:user, commit_email: Gitlab::PrivateCommitEmail::TOKEN)
+
+ expect(user).to be_valid
+ end
+
+ it 'accepts nil commit email' do
+ user = build(:user, commit_email: nil)
+
+ expect(user).to be_valid
+ end
+ end
end
end
@@ -1075,6 +1115,14 @@ describe User do
end
describe '.find_by_any_email' do
+ it 'finds user through private commit email' do
+ user = create(:user)
+ private_email = user.private_commit_email
+
+ expect(described_class.find_by_any_email(private_email)).to eq(user)
+ expect(described_class.find_by_any_email(private_email, confirmed: true)).to eq(user)
+ end
+
it 'finds by primary email' do
user = create(:user, email: 'foo@example.com')
@@ -1082,6 +1130,13 @@ describe User do
expect(described_class.find_by_any_email(user.email, confirmed: true)).to eq user
end
+ it 'finds by uppercased email' do
+ user = create(:user, email: 'foo@example.com')
+
+ expect(described_class.find_by_any_email(user.email.upcase)).to eq user
+ expect(described_class.find_by_any_email(user.email.upcase, confirmed: true)).to eq user
+ end
+
it 'finds by secondary email' do
email = create(:email, email: 'foo@example.com')
user = email.user
@@ -1457,7 +1512,7 @@ describe User do
email_confirmed = create :email, user: user, confirmed_at: Time.now
create :email, user: user
- expect(user.verified_emails).to match_array([user.email, email_confirmed.email])
+ expect(user.verified_emails).to match_array([user.email, user.private_commit_email, email_confirmed.email])
end
end
@@ -1473,6 +1528,10 @@ describe User do
expect(user.verified_email?(email_confirmed.email.titlecase)).to be_truthy
end
+ it 'returns true when user is found through private commit email' do
+ expect(user.verified_email?(user.private_commit_email)).to be_truthy
+ end
+
it 'returns false when the email is not verified/confirmed' do
email_unconfirmed = create :email, user: user
user.reload
@@ -1668,6 +1727,24 @@ describe User do
end
end
+ describe '.find_by_private_commit_email' do
+ context 'with email' do
+ set(:user) { create(:user) }
+
+ it 'returns user through private commit email' do
+ expect(described_class.find_by_private_commit_email(user.private_commit_email)).to eq(user)
+ end
+
+ it 'returns nil when email other than private_commit_email is used' do
+ expect(described_class.find_by_private_commit_email(user.email)).to be_nil
+ end
+ end
+
+ it 'returns nil when email is nil' do
+ expect(described_class.find_by_private_commit_email(nil)).to be_nil
+ end
+ end
+
describe '#sort_by_attribute' do
before do
described_class.delete_all
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index 821946a47e5..b87a2d871e5 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -126,23 +126,34 @@ describe WikiPage do
end
end
- before do
- @wiki_attr = { title: "Index", content: "Home Page", format: "markdown" }
- end
-
describe "#create" do
+ let(:wiki_attr) do
+ {
+ title: "Index",
+ content: "Home Page",
+ format: "markdown",
+ message: 'Custom Commit Message'
+ }
+ end
+
after do
destroy_page("Index")
end
context "with valid attributes" do
it "saves the wiki page" do
- subject.create(@wiki_attr)
+ subject.create(wiki_attr)
expect(wiki.find_page("Index")).not_to be_nil
end
it "returns true" do
- expect(subject.create(@wiki_attr)).to eq(true)
+ expect(subject.create(wiki_attr)).to eq(true)
+ end
+
+ it 'saves the wiki page with message' do
+ subject.create(wiki_attr)
+
+ expect(wiki.find_page("Index").message).to eq 'Custom Commit Message'
end
end
end
diff --git a/spec/policies/clusters/cluster_policy_spec.rb b/spec/policies/clusters/cluster_policy_spec.rb
index ced969830d8..b2f0ca1bc30 100644
--- a/spec/policies/clusters/cluster_policy_spec.rb
+++ b/spec/policies/clusters/cluster_policy_spec.rb
@@ -24,5 +24,47 @@ describe Clusters::ClusterPolicy, :models do
it { expect(policy).to be_allowed :update_cluster }
it { expect(policy).to be_allowed :admin_cluster }
end
+
+ context 'group cluster' do
+ let(:cluster) { create(:cluster, :group) }
+ let(:group) { cluster.group }
+ let(:project) { create(:project, namespace: group) }
+
+ context 'when group developer' do
+ before do
+ group.add_developer(user)
+ end
+
+ it { expect(policy).to be_disallowed :update_cluster }
+ it { expect(policy).to be_disallowed :admin_cluster }
+ end
+
+ context 'when group maintainer' do
+ before do
+ group.add_maintainer(user)
+ end
+
+ it { expect(policy).to be_allowed :update_cluster }
+ it { expect(policy).to be_allowed :admin_cluster }
+ end
+
+ context 'when project maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it { expect(policy).to be_disallowed :update_cluster }
+ it { expect(policy).to be_disallowed :admin_cluster }
+ end
+
+ context 'when project developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ 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/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index 5e583be457e..9d0093e8159 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -21,7 +21,11 @@ describe GroupPolicy do
let(:maintainer_permissions) do
[
- :create_projects
+ :create_projects,
+ :read_cluster,
+ :create_cluster,
+ :update_cluster,
+ :admin_cluster
]
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index b7ec35d6ec5..d6bc67a9d70 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -163,7 +163,7 @@ describe ProjectPolicy do
:create_build, :read_build, :update_build, :admin_build, :destroy_build,
:create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule,
:create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment,
- :create_cluster, :read_cluster, :update_cluster, :admin_cluster, :destroy_cluster,
+ :create_cluster, :read_cluster, :update_cluster, :admin_cluster,
:create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment
]
@@ -182,7 +182,7 @@ describe ProjectPolicy do
:create_build, :read_build, :update_build, :admin_build, :destroy_build,
:create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule,
:create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment,
- :create_cluster, :read_cluster, :update_cluster, :admin_cluster, :destroy_cluster,
+ :create_cluster, :read_cluster, :update_cluster, :admin_cluster,
:create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment
]
diff --git a/spec/presenters/blob_presenter_spec.rb b/spec/presenters/blob_presenter_spec.rb
new file mode 100644
index 00000000000..e85e7a41017
--- /dev/null
+++ b/spec/presenters/blob_presenter_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe BlobPresenter, :seed_helper do
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
+
+ let(:git_blob) do
+ Gitlab::Git::Blob.find(
+ repository,
+ 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6',
+ 'files/ruby/regex.rb'
+ )
+ end
+ let(:blob) { Blob.new(git_blob) }
+
+ describe '#highlight' do
+ subject { described_class.new(blob) }
+
+ it 'returns highlighted content' do
+ expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: nil, language: nil)
+
+ subject.highlight
+ end
+
+ it 'returns plain content when :plain is true' do
+ expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: true, language: nil)
+
+ subject.highlight(plain: true)
+ end
+
+ context 'gitlab-language contains a match' do
+ before do
+ allow(blob).to receive(:language_from_gitattributes).and_return('ruby')
+ end
+
+ it 'passes language to inner call' do
+ expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: nil, language: 'ruby')
+
+ subject.highlight
+ end
+ end
+ end
+end
diff --git a/spec/presenters/ci/build_presenter_spec.rb b/spec/presenters/ci/build_presenter_spec.rb
index d7992f0a4a9..676835b3880 100644
--- a/spec/presenters/ci/build_presenter_spec.rb
+++ b/spec/presenters/ci/build_presenter_spec.rb
@@ -267,7 +267,7 @@ describe Ci::BuildPresenter do
let(:build) { create(:ci_build, :failed, :script_failure) }
context 'when is a script or missing dependency failure' do
- let(:failure_reasons) { %w(script_failure missing_dependency_failure) }
+ let(:failure_reasons) { %w(script_failure missing_dependency_failure archived_failure) }
it 'should return false' do
failure_reasons.each do |failure_reason|
diff --git a/spec/presenters/clusterable_presenter_spec.rb b/spec/presenters/clusterable_presenter_spec.rb
new file mode 100644
index 00000000000..4f4ae5e07c5
--- /dev/null
+++ b/spec/presenters/clusterable_presenter_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ClusterablePresenter do
+ include Gitlab::Routing.url_helpers
+
+ describe '.fabricate' do
+ let(:project) { create(:project) }
+
+ subject { described_class.fabricate(project) }
+
+ it 'creates an object from a descendant presenter' do
+ expect(subject).to be_kind_of(ProjectClusterablePresenter)
+ end
+ end
+end
diff --git a/spec/presenters/clusters/cluster_presenter_spec.rb b/spec/presenters/clusters/cluster_presenter_spec.rb
index e96dbfb73c0..72c5eac3ede 100644
--- a/spec/presenters/clusters/cluster_presenter_spec.rb
+++ b/spec/presenters/clusters/cluster_presenter_spec.rb
@@ -1,7 +1,9 @@
require 'spec_helper'
describe Clusters::ClusterPresenter do
- let(:cluster) { create(:cluster, :provided_by_gcp) }
+ include Gitlab::Routing.url_helpers
+
+ let(:cluster) { create(:cluster, :provided_by_gcp, :project) }
subject(:presenter) do
described_class.new(cluster)
@@ -71,4 +73,21 @@ describe Clusters::ClusterPresenter do
it { is_expected.to eq(false) }
end
end
+
+ describe '#show_path' do
+ subject { described_class.new(cluster).show_path }
+
+ context 'project_type cluster' do
+ let(:project) { cluster.project }
+
+ it { is_expected.to eq(project_cluster_path(project, cluster)) }
+ end
+
+ context 'group_type cluster' do
+ let(:group) { cluster.group }
+ let(:cluster) { create(:cluster, :provided_by_gcp, :group) }
+
+ it { is_expected.to eq(group_cluster_path(group, cluster)) }
+ end
+ end
end
diff --git a/spec/presenters/group_clusterable_presenter_spec.rb b/spec/presenters/group_clusterable_presenter_spec.rb
new file mode 100644
index 00000000000..205160742bf
--- /dev/null
+++ b/spec/presenters/group_clusterable_presenter_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GroupClusterablePresenter do
+ include Gitlab::Routing.url_helpers
+
+ let(:presenter) { described_class.new(group) }
+ let(:cluster) { create(:cluster, :provided_by_gcp, :group) }
+ let(:group) { cluster.group }
+
+ describe '#can_create_cluster?' do
+ let(:user) { create(:user) }
+
+ subject { presenter.can_create_cluster? }
+
+ before do
+ allow(presenter).to receive(:current_user).and_return(user)
+ end
+
+ context 'when user can create' do
+ before do
+ group.add_maintainer(user)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when user cannot create' do
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#index_path' do
+ subject { presenter.index_path }
+
+ it { is_expected.to eq(group_clusters_path(group)) }
+ end
+
+ describe '#new_path' do
+ subject { presenter.new_path }
+
+ it { is_expected.to eq(new_group_cluster_path(group)) }
+ end
+
+ describe '#create_user_clusters_path' do
+ subject { presenter.create_user_clusters_path }
+
+ it { is_expected.to eq(create_user_group_clusters_path(group)) }
+ end
+
+ describe '#create_gcp_clusters_path' do
+ subject { presenter.create_gcp_clusters_path }
+
+ it { is_expected.to eq(create_gcp_group_clusters_path(group)) }
+ end
+
+ describe '#cluster_status_cluster_path' do
+ subject { presenter.cluster_status_cluster_path(cluster) }
+
+ it { is_expected.to eq(cluster_status_group_cluster_path(group, cluster)) }
+ end
+
+ describe '#install_applications_cluster_path' do
+ let(:application) { :helm }
+
+ subject { presenter.install_applications_cluster_path(cluster, application) }
+
+ it { is_expected.to eq(install_applications_group_cluster_path(group, cluster, application)) }
+ end
+
+ describe '#cluster_path' do
+ subject { presenter.cluster_path(cluster) }
+
+ it { is_expected.to eq(group_cluster_path(group, cluster)) }
+ end
+end
diff --git a/spec/presenters/project_clusterable_presenter_spec.rb b/spec/presenters/project_clusterable_presenter_spec.rb
new file mode 100644
index 00000000000..c50d90ae1e8
--- /dev/null
+++ b/spec/presenters/project_clusterable_presenter_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ProjectClusterablePresenter do
+ include Gitlab::Routing.url_helpers
+
+ let(:presenter) { described_class.new(project) }
+ let(:project) { create(:project) }
+ let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+
+ describe '#can_create_cluster?' do
+ let(:user) { create(:user) }
+
+ subject { presenter.can_create_cluster? }
+
+ before do
+ allow(presenter).to receive(:current_user).and_return(user)
+ end
+
+ context 'when user can create' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when user cannot create' do
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#index_path' do
+ subject { presenter.index_path }
+
+ it { is_expected.to eq(project_clusters_path(project)) }
+ end
+
+ describe '#new_path' do
+ subject { presenter.new_path }
+
+ it { is_expected.to eq(new_project_cluster_path(project)) }
+ end
+
+ describe '#create_user_clusters_path' do
+ subject { presenter.create_user_clusters_path }
+
+ it { is_expected.to eq(create_user_project_clusters_path(project)) }
+ end
+
+ describe '#create_gcp_clusters_path' do
+ subject { presenter.create_gcp_clusters_path }
+
+ it { is_expected.to eq(create_gcp_project_clusters_path(project)) }
+ end
+
+ describe '#cluster_status_cluster_path' do
+ subject { presenter.cluster_status_cluster_path(cluster) }
+
+ it { is_expected.to eq(cluster_status_project_cluster_path(project, cluster)) }
+ end
+
+ describe '#install_applications_cluster_path' do
+ let(:application) { :helm }
+
+ subject { presenter.install_applications_cluster_path(cluster, application) }
+
+ it { is_expected.to eq(install_applications_project_cluster_path(project, cluster, application)) }
+ end
+
+ describe '#cluster_path' do
+ subject { presenter.cluster_path(cluster) }
+
+ it { is_expected.to eq(project_cluster_path(project, cluster)) }
+ end
+end
diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb
index 3eb2f149311..7b0192fa9c8 100644
--- a/spec/presenters/project_presenter_spec.rb
+++ b/spec/presenters/project_presenter_spec.rb
@@ -239,7 +239,7 @@ describe ProjectPresenter do
expect(presenter.new_file_anchor_data).to have_attributes(enabled: false,
label: "New file",
link: presenter.project_new_blob_path(project, 'master'),
- class_modifier: 'new')
+ class_modifier: 'success')
end
it 'returns nil if user cannot push' do
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 98399471f9a..329d069ef3d 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -565,7 +565,7 @@ describe API::Commits do
}
end
- it 'are commited as one in project repo' do
+ it 'are committed as one in project repo' do
post api(url, user), valid_mo_params
expect(response).to have_gitlab_http_status(201)
@@ -1208,6 +1208,118 @@ describe API::Commits do
end
end
+ describe 'POST :id/repository/commits/:sha/revert' do
+ let(:commit_id) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
+ let(:commit) { project.commit(commit_id) }
+ let(:branch) { 'master' }
+ let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}/revert" }
+
+ shared_examples_for 'ref revert' do
+ context 'when ref exists' do
+ it 'reverts the ref commit' do
+ post api(route, current_user), branch: branch
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(response).to match_response_schema('public_api/v4/commit/basic')
+
+ expect(json_response['message']).to eq(commit.revert_message(user))
+ expect(json_response['author_name']).to eq(user.name)
+ expect(json_response['committer_name']).to eq(user.name)
+ expect(json_response['parent_ids']).to contain_exactly(commit_id)
+ end
+ end
+
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
+
+ it_behaves_like '403 response' do
+ let(:request) { post api(route, current_user), branch: branch }
+ end
+ end
+ end
+
+ context 'when unauthenticated', 'and project is public' do
+ let(:project) { create(:project, :public, :repository) }
+
+ it_behaves_like '403 response' do
+ let(:request) { post api(route), branch: branch }
+ end
+ end
+
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { post api(route), branch: branch }
+ let(:message) { '404 Project Not Found' }
+ end
+ end
+
+ context 'when authenticated', 'as an owner' do
+ let(:current_user) { user }
+
+ it_behaves_like 'ref revert'
+
+ context 'when ref does not exist' do
+ let(:commit_id) { 'unknown' }
+
+ it_behaves_like '404 response' do
+ let(:request) { post api(route, current_user), branch: branch }
+ let(:message) { '404 Commit Not Found' }
+ end
+ end
+
+ context 'when branch is missing' do
+ it_behaves_like '400 response' do
+ let(:request) { post api(route, current_user) }
+ end
+ end
+
+ context 'when branch is empty' do
+ ['', ' '].each do |branch|
+ it_behaves_like '400 response' do
+ let(:request) { post api(route, current_user), branch: branch }
+ end
+ end
+ end
+
+ context 'when branch does not exist' do
+ it_behaves_like '404 response' do
+ let(:request) { post api(route, current_user), branch: 'foo' }
+ let(:message) { '404 Branch Not Found' }
+ end
+ end
+
+ context 'when ref contains a dot' do
+ let(:commit_id) { branch_with_dot.name }
+ let(:commit) { project.repository.commit(commit_id) }
+
+ it_behaves_like '400 response' do
+ let(:request) { post api(route, current_user) }
+ end
+ end
+ end
+
+ context 'when authenticated', 'as a developer' do
+ let(:current_user) { user }
+
+ before do
+ project.add_developer(user)
+ end
+
+ context 'when branch is protected' do
+ before do
+ create(:protected_branch, project: project, name: 'feature')
+ end
+
+ it 'returns 400 if you are not allowed to push to the target branch' do
+ post api(route, current_user), branch: 'feature'
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ expect(json_response['message']).to match(/You are not allowed to push into this branch/)
+ end
+ end
+ end
+ end
+
describe 'POST /projects/:id/repository/commits/:sha/comments' do
let(:commit) { project.repository.commit }
let(:commit_id) { commit.id }
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
index 61ae053cea7..3dac7225b7a 100644
--- a/spec/requests/api/deployments_spec.rb
+++ b/spec/requests/api/deployments_spec.rb
@@ -10,9 +10,9 @@ describe API::Deployments do
describe 'GET /projects/:id/deployments' do
let(:project) { create(:project) }
- let!(:deployment_1) { create(:deployment, project: project, iid: 11, ref: 'master', created_at: Time.now) }
- let!(:deployment_2) { create(:deployment, project: project, iid: 12, ref: 'feature', created_at: 1.day.ago) }
- let!(:deployment_3) { create(:deployment, project: project, iid: 8, ref: 'feature', created_at: 2.days.ago) }
+ let!(:deployment_1) { create(:deployment, :success, project: project, iid: 11, ref: 'master', created_at: Time.now) }
+ let!(:deployment_2) { create(:deployment, :success, project: project, iid: 12, ref: 'feature', created_at: 1.day.ago) }
+ let!(:deployment_3) { create(:deployment, :success, project: project, iid: 8, ref: 'patch', created_at: 2.days.ago) }
context 'as member of the project' do
it 'returns projects deployments sorted by id asc' do
@@ -53,8 +53,8 @@ describe API::Deployments do
'id' | 'desc' | [:deployment_3, :deployment_2, :deployment_1]
'iid' | 'asc' | [:deployment_3, :deployment_1, :deployment_2]
'iid' | 'desc' | [:deployment_2, :deployment_1, :deployment_3]
- 'ref' | 'asc' | [:deployment_2, :deployment_3, :deployment_1]
- 'ref' | 'desc' | [:deployment_1, :deployment_2, :deployment_3]
+ 'ref' | 'asc' | [:deployment_2, :deployment_1, :deployment_3]
+ 'ref' | 'desc' | [:deployment_3, :deployment_1, :deployment_2]
end
with_them do
@@ -76,7 +76,7 @@ describe API::Deployments do
describe 'GET /projects/:id/deployments/:deployment_id' do
let(:project) { deployment.environment.project }
- let!(:deployment) { create(:deployment) }
+ let!(:deployment) { create(:deployment, :success) }
context 'as a member of the project' do
it 'returns the projects deployment' do
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 5ea869796b0..2ebcb787d06 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -683,7 +683,7 @@ describe API::Internal do
expect(json_response).to match [{
"branch_name" => "new_branch",
- "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new/new_branch",
+ "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch",
"new_merge_request" => true
}]
end
@@ -704,7 +704,7 @@ describe API::Internal do
expect(json_response).to match [{
"branch_name" => "new_branch",
- "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new/new_branch",
+ "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch",
"new_merge_request" => true
}]
end
@@ -837,7 +837,7 @@ describe API::Internal do
expect(json_response['merge_request_urls']).to match [{
"branch_name" => "new_branch",
- "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new/new_branch",
+ "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch",
"new_merge_request" => true
}]
end
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 9cda39a569b..3d532dd83c7 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -55,8 +55,8 @@ describe API::Issues do
end
let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
- let(:no_milestone_title) { URI.escape(Milestone::None.title) }
- let(:any_milestone_title) { URI.escape(Milestone::Any.title) }
+ let(:no_milestone_title) { "None" }
+ let(:any_milestone_title) { "Any" }
before(:all) do
project.add_reporter(user)
@@ -196,14 +196,24 @@ describe API::Issues do
expect_paginated_array_response(size: 3)
end
- it 'returns issues reacted by the authenticated user by the given emoji' do
+ it 'returns issues reacted by the authenticated user' do
issue2 = create(:issue, project: project, author: user, assignees: [user])
- award_emoji = create(:award_emoji, awardable: issue2, user: user2, name: 'star')
+ create(:award_emoji, awardable: issue2, user: user2, name: 'star')
- get api('/issues', user2), my_reaction_emoji: award_emoji.name, scope: 'all'
+ create(:award_emoji, awardable: issue, user: user2, name: 'thumbsup')
- expect_paginated_array_response(size: 1)
- expect(first_issue['id']).to eq(issue2.id)
+ get api('/issues', user2), my_reaction_emoji: 'Any', scope: 'all'
+
+ expect_paginated_array_response(size: 2)
+ end
+
+ it 'returns issues not reacted by the authenticated user' do
+ issue2 = create(:issue, project: project, author: user, assignees: [user])
+ create(:award_emoji, awardable: issue2, user: user2, name: 'star')
+
+ get api('/issues', user2), my_reaction_emoji: 'None', scope: 'all'
+
+ expect_paginated_array_response(size: 2)
end
it 'returns issues matching given search string for title' do
@@ -1791,6 +1801,74 @@ describe API::Issues do
end
end
+ describe 'GET :id/issues/:issue_iid/related_merge_requests' do
+ def get_related_merge_requests(project_id, issue_iid, user = nil)
+ get api("/projects/#{project_id}/issues/#{issue_iid}/related_merge_requests", user)
+ end
+
+ def create_referencing_mr(user, project, issue)
+ attributes = {
+ author: user,
+ source_project: project,
+ target_project: project,
+ source_branch: "master",
+ target_branch: "test",
+ description: "See #{issue.to_reference}"
+ }
+ create(:merge_request, attributes).tap do |merge_request|
+ create(:note, :system, project: project, noteable: issue, author: user, note: merge_request.to_reference(full: true))
+ end
+ end
+
+ let!(:related_mr) { create_referencing_mr(user, project, issue) }
+
+ context 'when unauthenticated' do
+ it 'return list of referenced merge requests from issue' do
+ get_related_merge_requests(project.id, issue.iid)
+
+ expect_paginated_array_response(size: 1)
+ end
+
+ it 'renders 404 if project is not visible' do
+ private_project = create(:project, :private)
+ private_issue = create(:issue, project: private_project)
+ create_referencing_mr(user, private_project, private_issue)
+
+ get_related_merge_requests(private_project.id, private_issue.iid)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ it 'returns merge requests that mentioned a issue' do
+ create(:merge_request,
+ :simple,
+ author: user,
+ source_project: project,
+ target_project: project,
+ description: "Some description")
+
+ get_related_merge_requests(project.id, issue.iid, user)
+
+ expect_paginated_array_response(size: 1)
+ expect(json_response.first['id']).to eq(related_mr.id)
+ end
+
+ context 'no merge request mentioned a issue' do
+ it 'returns empty array' do
+ get_related_merge_requests(project.id, closed_issue.iid, user)
+
+ expect_paginated_array_response(size: 0)
+ end
+ end
+
+ it "returns 404 when issue doesn't exists" do
+ get_related_merge_requests(project.id, 999999, user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
describe "GET /projects/:id/issues/:issue_iid/user_agent_detail" do
let!(:user_agent_detail) { create(:user_agent_detail, subject: issue) }
diff --git a/spec/requests/api/submodules_spec.rb b/spec/requests/api/submodules_spec.rb
new file mode 100644
index 00000000000..fa447c028c2
--- /dev/null
+++ b/spec/requests/api/submodules_spec.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::Submodules do
+ let(:user) { create(:user) }
+ let!(:project) { create(:project, :repository, namespace: user.namespace ) }
+ let(:guest) { create(:user) { |u| project.add_guest(u) } }
+ let(:submodule) { 'six' }
+ let(:commit_sha) { 'e25eda1fece24ac7a03624ed1320f82396f35bd8' }
+ let(:branch) { 'master' }
+ let(:commit_message) { 'whatever' }
+
+ let(:params) do
+ {
+ submodule: submodule,
+ commit_sha: commit_sha,
+ branch: branch,
+ commit_message: commit_message
+ }
+ end
+
+ before do
+ project.add_developer(user)
+ end
+
+ def route(submodule = nil)
+ "/projects/#{project.id}/repository/submodules/#{submodule}"
+ end
+
+ describe "PUT /projects/:id/repository/submodule/:submodule" do
+ context 'when unauthenticated' do
+ it 'returns 401' do
+ put api(route(submodule)), params
+
+ expect(response).to have_gitlab_http_status(401)
+ end
+ end
+
+ context 'when authenticated', 'as a guest' do
+ it 'returns 403' do
+ put api(route(submodule), guest), params
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+
+ context 'when authenticated', 'as a developer' do
+ it 'returns 400 if params is missing' do
+ put api(route(submodule), user)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it 'returns 400 if branch is missing' do
+ put api(route(submodule), user), params.except(:branch)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it 'returns 400 if commit_sha is missing' do
+ put api(route(submodule), user), params.except(:commit_sha)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it 'returns the commmit' do
+ head_commit = project.repository.commit.id
+
+ put api(route(submodule), user), params
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['message']).to eq commit_message
+ expect(json_response['author_name']).to eq user.name
+ expect(json_response['committer_name']).to eq user.name
+ expect(json_response['parent_ids'].first).to eq head_commit
+ end
+
+ context 'when the submodule name is urlencoded' do
+ let(:submodule) { 'test_inside_folder/another_folder/six' }
+ let(:branch) { 'submodule_inside_folder' }
+ let(:encoded_submodule) { CGI.escape(submodule) }
+
+ it 'returns the commmit' do
+ expect(Submodules::UpdateService)
+ .to receive(:new)
+ .with(any_args, hash_including(submodule: submodule))
+ .and_call_original
+
+ put api(route(encoded_submodule), user), params
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['id']).to eq project.repository.commit(branch).id
+ expect(project.repository.blob_at(branch, submodule).id).to eq commit_sha
+ end
+ end
+ end
+ end
+end
diff --git a/spec/serializers/build_action_entity_spec.rb b/spec/serializers/build_action_entity_spec.rb
index 9e2bee2ee60..ea88951ebc6 100644
--- a/spec/serializers/build_action_entity_spec.rb
+++ b/spec/serializers/build_action_entity_spec.rb
@@ -26,6 +26,10 @@ describe BuildActionEntity do
context 'when job is scheduled' do
let(:job) { create(:ci_build, :scheduled) }
+ it 'returns scheduled' do
+ expect(subject[:scheduled]).to be_truthy
+ end
+
it 'returns scheduled_at' do
expect(subject[:scheduled_at]).to eq(job.scheduled_at)
end
diff --git a/spec/serializers/deployment_entity_spec.rb b/spec/serializers/deployment_entity_spec.rb
index 522c92ce295..8793a762f9d 100644
--- a/spec/serializers/deployment_entity_spec.rb
+++ b/spec/serializers/deployment_entity_spec.rb
@@ -22,4 +22,26 @@ describe DeploymentEntity do
it 'exposes creation date' do
expect(subject).to include(:created_at)
end
+
+ describe 'scheduled_actions' do
+ let(:project) { create(:project, :repository) }
+ let(:pipeline) { create(:ci_pipeline, project: project, user: user) }
+ let(:build) { create(:ci_build, :success, pipeline: pipeline) }
+ let(:deployment) { create(:deployment, deployable: build) }
+
+ context 'when the same pipeline has a scheduled action' do
+ let(:other_build) { create(:ci_build, :schedulable, :success, pipeline: pipeline, name: 'other build') }
+ let!(:other_deployment) { create(:deployment, deployable: other_build) }
+
+ it 'returns other scheduled actions' do
+ expect(subject[:scheduled_actions][0][:name]).to eq 'other build'
+ end
+ end
+
+ context 'when the same pipeline does not have a scheduled action' do
+ it 'does not return other actions' do
+ expect(subject[:scheduled_actions]).to be_empty
+ end
+ end
+ end
end
diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb
index 0f0ab5ac796..87493a28d1f 100644
--- a/spec/serializers/environment_serializer_spec.rb
+++ b/spec/serializers/environment_serializer_spec.rb
@@ -14,7 +14,8 @@ describe EnvironmentSerializer do
let(:project) { create(:project, :repository) }
let(:deployable) { create(:ci_build) }
let(:deployment) do
- create(:deployment, deployable: deployable,
+ create(:deployment, :success,
+ deployable: deployable,
user: user,
project: project,
sha: project.commit.id)
diff --git a/spec/serializers/environment_status_entity_spec.rb b/spec/serializers/environment_status_entity_spec.rb
index 1b4d8b70aa6..8a6a38fe5f8 100644
--- a/spec/serializers/environment_status_entity_spec.rb
+++ b/spec/serializers/environment_status_entity_spec.rb
@@ -4,8 +4,8 @@ describe EnvironmentStatusEntity do
let(:user) { create(:user) }
let(:request) { double('request') }
- let(:deployment) { create(:deployment, :review_app) }
- let(:environment) { deployment.environment}
+ let(:deployment) { create(:deployment, :succeed, :review_app) }
+ let(:environment) { deployment.environment }
let(:project) { deployment.project }
let(:merge_request) { create(:merge_request, :deployed_review_app, deployment: deployment) }
@@ -15,6 +15,7 @@ describe EnvironmentStatusEntity do
subject { entity.as_json }
before do
+ deployment.update(sha: merge_request.diff_head_sha)
allow(request).to receive(:current_user).and_return(user)
end
@@ -32,19 +33,47 @@ describe EnvironmentStatusEntity do
it { is_expected.not_to include(:metrics_url) }
it { is_expected.not_to include(:metrics_monitoring_url) }
- context 'when :ci_environments_status_changes feature flag is disabled' do
+ context 'when the user is project maintainer' do
before do
- stub_feature_flags(ci_environments_status_changes: false)
+ project.add_maintainer(user)
end
- it { is_expected.not_to include(:changes) }
+ it { is_expected.to include(:stop_url) }
end
- context 'when the user is project maintainer' do
+ context 'when deployment has metrics' do
+ let(:prometheus_adapter) { double('prometheus_adapter', can_query?: true) }
+
+ let(:simple_metrics) do
+ {
+ success: true,
+ metrics: {},
+ last_update: 42
+ }
+ end
+
before do
project.add_maintainer(user)
+ allow(deployment).to receive(:prometheus_adapter).and_return(prometheus_adapter)
+ allow(prometheus_adapter).to receive(:query).with(:deployment, deployment).and_return(simple_metrics)
+ allow(entity).to receive(:deployment).and_return(deployment)
end
- it { is_expected.to include(:stop_url) }
+ context 'when deployment succeeded' do
+ let(:deployment) { create(:deployment, :succeed, :review_app) }
+
+ it 'returns metrics url' do
+ expect(subject[:metrics_url])
+ .to eq("/#{project.namespace.name}/#{project.name}/environments/#{environment.id}/deployments/#{deployment.iid}/metrics")
+ end
+ end
+
+ context 'when deployment is running' do
+ let(:deployment) { create(:deployment, :running, :review_app) }
+
+ it 'does not return metrics url' do
+ expect(subject[:metrics_url]).to be_nil
+ end
+ end
end
end
diff --git a/spec/serializers/job_entity_spec.rb b/spec/serializers/job_entity_spec.rb
index 5fc27da4906..851b41a7f7e 100644
--- a/spec/serializers/job_entity_spec.rb
+++ b/spec/serializers/job_entity_spec.rb
@@ -117,6 +117,7 @@ describe JobEntity do
end
it 'contains scheduled_at' do
+ expect(subject[:scheduled]).to be_truthy
expect(subject[:scheduled_at]).to eq(job.scheduled_at)
end
end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 054b7b1561c..4d9c5aabbda 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -387,15 +387,24 @@ describe Ci::CreatePipelineService do
context 'with environment' do
before do
- config = YAML.dump(deploy: { environment: { name: "review/$CI_COMMIT_REF_NAME" }, script: 'ls' })
+ config = YAML.dump(
+ deploy: {
+ environment: { name: "review/$CI_COMMIT_REF_NAME" },
+ script: 'ls',
+ tags: ['hello']
+ })
+
stub_ci_pipeline_yaml_file(config)
end
- it 'creates the environment' do
+ it 'creates the environment with tags' do
result = execute_service
expect(result).to be_persisted
expect(Environment.find_by(name: "review/master")).to be_present
+ expect(result.builds.first.tag_list).to contain_exactly('hello')
+ expect(result.builds.first.deployment).to be_persisted
+ expect(result.builds.first.deployment.deployable).to be_a(Ci::Build)
end
end
@@ -435,16 +444,34 @@ describe Ci::CreatePipelineService do
end
context 'when builds with auto-retries are configured' do
- before do
- config = YAML.dump(rspec: { script: 'rspec', retry: 2 })
- stub_ci_pipeline_yaml_file(config)
+ context 'as an integer' do
+ before do
+ config = YAML.dump(rspec: { script: 'rspec', retry: 2 })
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ it 'correctly creates builds with auto-retry value configured' do
+ pipeline = execute_service
+
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.find_by(name: 'rspec').retries_max).to eq 2
+ expect(pipeline.builds.find_by(name: 'rspec').retry_when).to eq ['always']
+ end
end
- it 'correctly creates builds with auto-retry value configured' do
- pipeline = execute_service
+ context 'as hash' do
+ before do
+ config = YAML.dump(rspec: { script: 'rspec', retry: { max: 2, when: 'runner_system_failure' } })
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ it 'correctly creates builds with auto-retry value configured' do
+ pipeline = execute_service
- expect(pipeline).to be_persisted
- expect(pipeline.builds.find_by(name: 'rspec').retries_max).to eq 2
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.find_by(name: 'rspec').retries_max).to eq 2
+ expect(pipeline.builds.find_by(name: 'rspec').retry_when).to eq ['runner_system_failure']
+ end
end
end
@@ -581,5 +608,53 @@ describe Ci::CreatePipelineService do
.to eq variables_attributes.map(&:with_indifferent_access)
end
end
+
+ context 'when pipeline has a job with environment' do
+ let(:pipeline) { execute_service }
+
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump(config))
+ end
+
+ context 'when environment name is valid' do
+ let(:config) do
+ {
+ review_app: {
+ script: 'deploy',
+ environment: {
+ name: 'review/${CI_COMMIT_REF_NAME}',
+ url: 'http://${CI_COMMIT_REF_SLUG}-staging.example.com'
+ }
+ }
+ }
+ end
+
+ it 'has a job with environment' do
+ expect(pipeline.builds.count).to eq(1)
+ expect(pipeline.builds.first.persisted_environment.name).to eq('review/master')
+ expect(pipeline.builds.first.deployment).to be_created
+ end
+ end
+
+ context 'when environment name is invalid' do
+ let(:config) do
+ {
+ 'job:deploy-to-test-site': {
+ script: 'deploy',
+ environment: {
+ name: '${CI_JOB_NAME}',
+ url: 'https://$APP_URL'
+ }
+ }
+ }
+ end
+
+ it 'has a job without environment' do
+ expect(pipeline.builds.count).to eq(1)
+ expect(pipeline.builds.first.persisted_environment).to be_nil
+ expect(pipeline.builds.first.deployment).to be_nil
+ end
+ end
+ end
end
end
diff --git a/spec/services/ci/process_build_service_spec.rb b/spec/services/ci/process_build_service_spec.rb
index 9a53b32394d..704685417bb 100644
--- a/spec/services/ci/process_build_service_spec.rb
+++ b/spec/services/ci/process_build_service_spec.rb
@@ -98,47 +98,19 @@ describe Ci::ProcessBuildService, '#execute' do
let(:build) { create(:ci_build, :created, :schedulable, user: user, project: project) }
- context 'when ci_enable_scheduled_build is enabled' do
- before do
- stub_feature_flags(ci_enable_scheduled_build: true)
- end
-
- context 'when current status is success' do
- let(:current_status) { 'success' }
-
- it 'changes the build status' do
- expect { subject }.to change { build.status }.to('scheduled')
- end
- end
-
- context 'when current status is failed' do
- let(:current_status) { 'failed' }
+ context 'when current status is success' do
+ let(:current_status) { 'success' }
- it 'does not change the build status' do
- expect { subject }.to change { build.status }.to('skipped')
- end
+ it 'changes the build status' do
+ expect { subject }.to change { build.status }.to('scheduled')
end
end
- context 'when ci_enable_scheduled_build is disabled' do
- before do
- stub_feature_flags(ci_enable_scheduled_build: false)
- end
-
- context 'when current status is success' do
- let(:current_status) { 'success' }
-
- it 'changes the build status' do
- expect { subject }.to change { build.status }.to('manual')
- end
- end
-
- context 'when current status is failed' do
- let(:current_status) { 'failed' }
+ context 'when current status is failed' do
+ let(:current_status) { 'failed' }
- it 'does not change the build status' do
- expect { subject }.to change { build.status }.to('skipped')
- end
+ it 'does not change the build status' do
+ expect { subject }.to change { build.status }.to('skipped')
end
end
end
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index 8c7258c42ad..538992b621e 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -671,9 +671,9 @@ describe Ci::ProcessPipelineService, '#execute' do
context 'when builds with auto-retries are configured' do
before do
- create_build('build:1', stage_idx: 0, user: user, options: { retry: 2 })
+ create_build('build:1', stage_idx: 0, user: user, options: { retry: { max: 2 } })
create_build('test:1', stage_idx: 1, user: user, when: :on_failure)
- create_build('test:2', stage_idx: 1, user: user, options: { retry: 1 })
+ create_build('test:2', stage_idx: 1, user: user, options: { retry: { max: 1 } })
end
it 'automatically retries builds in a valid order' do
diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
index a6565709641..56e2a405bcd 100644
--- a/spec/services/ci/register_job_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -478,6 +478,20 @@ module Ci
it_behaves_like 'validation is not active'
end
end
+
+ context 'when build is degenerated' do
+ let!(:pending_job) { create(:ci_build, :pending, :degenerated, pipeline: pipeline) }
+
+ subject { execute(specific_runner, {}) }
+
+ it 'does not pick the build and drops the build' do
+ expect(subject).to be_nil
+
+ pending_job.reload
+ expect(pending_job).to be_failed
+ expect(pending_job).to be_archived_failure
+ end
+ end
end
describe '#register_success' do
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 368abded448..e779675744c 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -32,7 +32,7 @@ describe Ci::RetryBuildService do
IGNORE_ACCESSORS =
%i[type lock_version target_url base_tags trace_sections
- commit_id deployments erased_by_id last_deployment project_id
+ commit_id deployment erased_by_id project_id
runner_id tag_taggings taggings tags trigger_request_id
user_id auto_canceled_by_id retried failure_reason
artifacts_file_store artifacts_metadata_store
diff --git a/spec/services/ci/run_scheduled_build_service_spec.rb b/spec/services/ci/run_scheduled_build_service_spec.rb
index 2c921dac238..be2aad33ef4 100644
--- a/spec/services/ci/run_scheduled_build_service_spec.rb
+++ b/spec/services/ci/run_scheduled_build_service_spec.rb
@@ -7,10 +7,6 @@ describe Ci::RunScheduledBuildService do
subject { described_class.new(project, user).execute(build) }
- before do
- stub_feature_flags(ci_enable_scheduled_build: true)
- end
-
context 'when user can update build' do
before do
project.add_developer(user)
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 1a565bb734d..ea17f2bb423 100644
--- a/spec/services/clusters/applications/check_installation_progress_service_spec.rb
+++ b/spec/services/clusters/applications/check_installation_progress_service_spec.rb
@@ -19,6 +19,10 @@ describe Clusters::Applications::CheckInstallationProgressService do
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 timeouted' do
it 'reschedule a new check' do
@@ -50,8 +54,6 @@ describe Clusters::Applications::CheckInstallationProgressService do
end
before do
- expect(service).to receive(:installation_phase).once.and_return(phase)
-
allow(service).to receive(:installation_errors).and_return(errors)
allow(service).to receive(:remove_installation_pod).and_return(nil)
end
@@ -60,6 +62,10 @@ describe Clusters::Applications::CheckInstallationProgressService do
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_behaves_like 'a terminated installation'
it 'make the application installed' do
@@ -76,6 +82,10 @@ describe Clusters::Applications::CheckInstallationProgressService 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_behaves_like 'a terminated installation'
it 'make the application errored' do
@@ -87,5 +97,22 @@ describe Clusters::Applications::CheckInstallationProgressService do
end
RESCHEDULE_PHASES.each { |phase| it_behaves_like 'a not yet terminated installation', phase }
+
+ context 'when installation raises a Kubeclient::HttpError' do
+ let(:cluster) { create(:cluster, :provided_by_user, :project) }
+
+ before do
+ application.update!(cluster: cluster)
+
+ expect(service).to receive(:installation_phase).and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil))
+ end
+
+ it 'shows the response code from the error' do
+ service.execute
+
+ expect(application).to be_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 056db0c5486..0bd7719345e 100644
--- a/spec/services/clusters/applications/create_service_spec.rb
+++ b/spec/services/clusters/applications/create_service_spec.rb
@@ -60,6 +60,31 @@ describe Clusters::Applications::CreateService do
end
end
+ context 'knative application' do
+ let(:params) do
+ {
+ application: 'knative',
+ hostname: 'example.com'
+ }
+ end
+
+ before do
+ allow_any_instance_of(Clusters::Applications::ScheduleInstallationService).to receive(:execute)
+ end
+
+ it 'creates the application' do
+ expect do
+ subject
+
+ cluster.reload
+ end.to change(cluster, :application_knative)
+ end
+
+ it 'sets the hostname' do
+ expect(subject.hostname).to eq('example.com')
+ end
+ end
+
context 'invalid application' do
let(:params) { { application: 'non-existent' } }
@@ -67,5 +92,39 @@ describe Clusters::Applications::CreateService do
expect { subject }.to raise_error(Clusters::Applications::CreateService::InvalidApplicationError)
end
end
+
+ context 'group cluster' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, :group) }
+
+ using RSpec::Parameterized::TableSyntax
+
+ before do
+ allow_any_instance_of(Clusters::Applications::ScheduleInstallationService).to receive(:execute)
+ end
+
+ where(:application, :association, :allowed) do
+ 'helm' | :application_helm | true
+ 'ingress' | :application_ingress | true
+ 'runner' | :application_runner | false
+ 'jupyter' | :application_jupyter | false
+ 'prometheus' | :application_prometheus | false
+ end
+
+ with_them do
+ let(:params) { { application: application } }
+
+ it 'executes for each application' do
+ if allowed
+ expect do
+ subject
+
+ cluster.reload
+ end.to change(cluster, association)
+ else
+ expect { subject }.to raise_error(Clusters::Applications::CreateService::InvalidApplicationError)
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/services/clusters/applications/install_service_spec.rb b/spec/services/clusters/applications/install_service_spec.rb
index 4bd19f5bd79..2f801d019fe 100644
--- a/spec/services/clusters/applications/install_service_spec.rb
+++ b/spec/services/clusters/applications/install_service_spec.rb
@@ -42,7 +42,7 @@ describe Clusters::Applications::InstallService do
service.execute
expect(application).to be_errored
- expect(application.status_reason).to match('Kubernetes error.')
+ expect(application.status_reason).to match('Kubernetes error: 500')
end
end
diff --git a/spec/services/clusters/create_service_spec.rb b/spec/services/clusters/create_service_spec.rb
index 3959295c13e..274880f2c49 100644
--- a/spec/services/clusters/create_service_spec.rb
+++ b/spec/services/clusters/create_service_spec.rb
@@ -5,18 +5,43 @@ describe Clusters::CreateService do
let(:project) { create(:project) }
let(:user) { create(:user) }
- subject { described_class.new(user, params).execute(project: project, access_token: access_token) }
+ subject { described_class.new(user, params).execute(access_token: access_token) }
context 'when provider is gcp' do
context 'when project has no clusters' do
context 'when correct params' do
- include_context 'valid cluster create params'
+ let(:params) do
+ {
+ name: 'test-cluster',
+ provider_type: :gcp,
+ provider_gcp_attributes: {
+ gcp_project_id: 'gcp-project',
+ zone: 'us-central1-a',
+ num_nodes: 1,
+ machine_type: 'machine_type-a',
+ legacy_abac: 'true'
+ },
+ clusterable: project
+ }
+ end
include_examples 'create cluster service success'
end
context 'when invalid params' do
- include_context 'invalid cluster create params'
+ let(:params) do
+ {
+ name: 'test-cluster',
+ provider_type: :gcp,
+ provider_gcp_attributes: {
+ gcp_project_id: '!!!!!!!',
+ zone: 'us-central1-a',
+ num_nodes: 1,
+ machine_type: 'machine_type-a'
+ },
+ clusterable: project
+ }
+ end
include_examples 'create cluster service error'
end
diff --git a/spec/services/clusters/gcp/fetch_operation_service_spec.rb b/spec/services/clusters/gcp/fetch_operation_service_spec.rb
index e2fa93904c5..55f123ee786 100644
--- a/spec/services/clusters/gcp/fetch_operation_service_spec.rb
+++ b/spec/services/clusters/gcp/fetch_operation_service_spec.rb
@@ -24,7 +24,7 @@ describe Clusters::Gcp::FetchOperationService do
end
end
- context 'when suceeded to fetch operation' do
+ context 'when succeeded to fetch operation' do
before do
stub_cloud_platform_get_zone_operation(gcp_project_id, zone, operation_id)
end
diff --git a/spec/services/clusters/gcp/finalize_creation_service_spec.rb b/spec/services/clusters/gcp/finalize_creation_service_spec.rb
index 0f484222228..efee158739d 100644
--- a/spec/services/clusters/gcp/finalize_creation_service_spec.rb
+++ b/spec/services/clusters/gcp/finalize_creation_service_spec.rb
@@ -1,156 +1,176 @@
+# frozen_string_literal: true
+
require 'spec_helper'
-describe Clusters::Gcp::FinalizeCreationService do
+describe Clusters::Gcp::FinalizeCreationService, '#execute' do
include GoogleApi::CloudPlatformHelpers
include KubernetesHelpers
- describe '#execute' do
- let(:cluster) { create(:cluster, :project, :providing_by_gcp) }
- let(:provider) { cluster.provider }
- let(:platform) { cluster.platform }
- let(:gcp_project_id) { provider.gcp_project_id }
- let(:zone) { provider.zone }
- let(:cluster_name) { cluster.name }
+ let(:cluster) { create(:cluster, :project, :providing_by_gcp) }
+ let(:provider) { cluster.provider }
+ let(:platform) { cluster.platform }
+ let(:endpoint) { '111.111.111.111' }
+ let(:api_url) { 'https://' + endpoint }
+ let(:username) { 'sample-username' }
+ let(:password) { 'sample-password' }
+ let(:secret_name) { 'gitlab-token' }
+ let(:token) { 'sample-token' }
+ let(:namespace) { "#{cluster.project.path}-#{cluster.project.id}" }
- subject { described_class.new.execute(provider) }
+ subject { described_class.new.execute(provider) }
- shared_examples 'success' do
- it 'configures provider and kubernetes' do
- subject
+ shared_examples 'success' do
+ it 'configures provider and kubernetes' do
+ subject
- expect(provider).to be_created
- end
+ expect(provider).to be_created
end
- shared_examples 'error' do
- it 'sets an error to provider object' do
- subject
+ it 'properly configures database models' do
+ subject
- expect(provider.reload).to be_errored
- end
+ cluster.reload
+
+ expect(provider.endpoint).to eq(endpoint)
+ expect(platform.api_url).to eq(api_url)
+ expect(platform.ca_cert).to eq(Base64.decode64(load_sample_cert).strip)
+ expect(platform.username).to eq(username)
+ expect(platform.password).to eq(password)
+ expect(platform.token).to eq(token)
+ end
+
+ it 'creates kubernetes namespace model' do
+ subject
+
+ kubernetes_namespace = cluster.reload.kubernetes_namespace
+ expect(kubernetes_namespace).to be_persisted
+ expect(kubernetes_namespace.namespace).to eq(namespace)
+ expect(kubernetes_namespace.service_account_name).to eq("#{namespace}-service-account")
+ expect(kubernetes_namespace.service_account_token).to be_present
end
+ end
+
+ shared_examples 'error' do
+ it 'sets an error to provider object' do
+ subject
- context 'when suceeded to fetch gke cluster info' do
- let(:endpoint) { '111.111.111.111' }
- let(:api_url) { 'https://' + endpoint }
- let(:username) { 'sample-username' }
- let(:password) { 'sample-password' }
- let(:secret_name) { 'gitlab-token' }
+ expect(provider.reload).to be_errored
+ end
+ end
+ shared_examples 'kubernetes information not successfully fetched' do
+ context 'when failed to fetch gke cluster info' do
before do
- stub_cloud_platform_get_zone_cluster(
- gcp_project_id, zone, cluster_name,
- {
- endpoint: endpoint,
- username: username,
- password: password
- }
- )
+ stub_cloud_platform_get_zone_cluster_error(provider.gcp_project_id, provider.zone, cluster.name)
end
- context 'service account and token created' do
- before do
- stub_kubeclient_discover(api_url)
- stub_kubeclient_create_service_account(api_url)
- stub_kubeclient_create_secret(api_url)
- end
-
- shared_context 'kubernetes token successfully fetched' do
- let(:token) { 'sample-token' }
-
- before do
- stub_kubeclient_get_secret(
- api_url,
- {
- metadata_name: secret_name,
- token: Base64.encode64(token)
- } )
- end
- end
-
- context 'provider legacy_abac is enabled' do
- include_context 'kubernetes token successfully fetched'
-
- it_behaves_like 'success'
-
- it 'properly configures database models' do
- subject
-
- cluster.reload
-
- expect(provider.endpoint).to eq(endpoint)
- expect(platform.api_url).to eq(api_url)
- expect(platform.ca_cert).to eq(Base64.decode64(load_sample_cert))
- expect(platform.username).to eq(username)
- expect(platform.password).to eq(password)
- expect(platform).to be_abac
- expect(platform.authorization_type).to eq('abac')
- expect(platform.token).to eq(token)
- end
- end
-
- context 'provider legacy_abac is disabled' do
- before do
- provider.legacy_abac = false
- end
-
- include_context 'kubernetes token successfully fetched'
-
- context 'cluster role binding created' do
- before do
- stub_kubeclient_create_cluster_role_binding(api_url)
- end
-
- it_behaves_like 'success'
-
- it 'properly configures database models' do
- subject
-
- cluster.reload
-
- expect(provider.endpoint).to eq(endpoint)
- expect(platform.api_url).to eq(api_url)
- expect(platform.ca_cert).to eq(Base64.decode64(load_sample_cert))
- expect(platform.username).to eq(username)
- expect(platform.password).to eq(password)
- expect(platform).to be_rbac
- expect(platform.token).to eq(token)
- end
- end
- end
-
- context 'when token is empty' do
- before do
- stub_kubeclient_get_secret(api_url, token: '', metadata_name: secret_name)
- end
-
- it_behaves_like 'error'
- end
-
- context 'when failed to fetch kubernetes token' do
- before do
- stub_kubeclient_get_secret_error(api_url, secret_name)
- end
-
- it_behaves_like 'error'
- end
-
- context 'when service account fails to create' do
- before do
- stub_kubeclient_create_service_account_error(api_url)
- end
-
- it_behaves_like 'error'
- end
+ it_behaves_like 'error'
+ end
+
+ context 'when token is empty' do
+ let(:token) { '' }
+
+ it_behaves_like 'error'
+ end
+
+ context 'when failed to fetch kubernetes token' do
+ before do
+ stub_kubeclient_get_secret_error(api_url, secret_name, namespace: 'default')
end
+
+ it_behaves_like 'error'
end
- context 'when failed to fetch gke cluster info' do
+ context 'when service account fails to create' do
before do
- stub_cloud_platform_get_zone_cluster_error(gcp_project_id, zone, cluster_name)
+ stub_kubeclient_create_service_account_error(api_url, namespace: 'default')
end
it_behaves_like 'error'
end
end
+
+ shared_context 'kubernetes information successfully fetched' do
+ before do
+ stub_cloud_platform_get_zone_cluster(
+ provider.gcp_project_id, provider.zone, cluster.name,
+ {
+ endpoint: endpoint,
+ username: username,
+ password: password
+ }
+ )
+
+ stub_kubeclient_discover(api_url)
+ stub_kubeclient_get_namespace(api_url)
+ stub_kubeclient_create_namespace(api_url)
+ stub_kubeclient_create_service_account(api_url)
+ stub_kubeclient_create_secret(api_url)
+
+ stub_kubeclient_get_secret(
+ api_url,
+ {
+ metadata_name: secret_name,
+ token: Base64.encode64(token),
+ namespace: 'default'
+ }
+ )
+
+ stub_kubeclient_get_namespace(api_url, namespace: namespace)
+ stub_kubeclient_create_service_account(api_url, namespace: namespace)
+ stub_kubeclient_create_secret(api_url, namespace: namespace)
+
+ stub_kubeclient_get_secret(
+ api_url,
+ {
+ metadata_name: "#{namespace}-token",
+ token: Base64.encode64(token),
+ namespace: namespace
+ }
+ )
+ end
+ end
+
+ context 'With a legacy ABAC cluster' do
+ before do
+ provider.legacy_abac = true
+ end
+
+ include_context 'kubernetes information successfully fetched'
+
+ it_behaves_like 'success'
+
+ it 'uses ABAC authorization type' do
+ subject
+ cluster.reload
+
+ expect(platform).to be_abac
+ expect(platform.authorization_type).to eq('abac')
+ end
+
+ it_behaves_like 'kubernetes information not successfully fetched'
+ end
+
+ context 'With an RBAC cluster' do
+ before do
+ provider.legacy_abac = false
+
+ stub_kubeclient_create_cluster_role_binding(api_url)
+ stub_kubeclient_create_role_binding(api_url, namespace: namespace)
+ end
+
+ include_context 'kubernetes information successfully fetched'
+
+ it_behaves_like 'success'
+
+ it 'uses RBAC authorization type' do
+ subject
+ cluster.reload
+
+ expect(platform).to be_rbac
+ expect(platform.authorization_type).to eq('rbac')
+ end
+
+ it_behaves_like 'kubernetes information not successfully fetched'
+ end
end
diff --git a/spec/services/clusters/gcp/kubernetes/create_or_update_namespace_service_spec.rb b/spec/services/clusters/gcp/kubernetes/create_or_update_namespace_service_spec.rb
new file mode 100644
index 00000000000..fc922218ad0
--- /dev/null
+++ b/spec/services/clusters/gcp/kubernetes/create_or_update_namespace_service_spec.rb
@@ -0,0 +1,115 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' do
+ include KubernetesHelpers
+
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:platform) { cluster.platform }
+ let(:api_url) { 'https://kubernetes.example.com' }
+ let(:project) { cluster.project }
+ let(:cluster_project) { cluster.cluster_project }
+
+ subject do
+ described_class.new(
+ cluster: cluster,
+ kubernetes_namespace: kubernetes_namespace
+ ).execute
+ end
+
+ shared_context 'kubernetes requests' do
+ before do
+ stub_kubeclient_discover(api_url)
+ stub_kubeclient_get_namespace(api_url)
+ stub_kubeclient_create_service_account(api_url)
+ stub_kubeclient_create_secret(api_url)
+
+ stub_kubeclient_get_namespace(api_url, namespace: namespace)
+ stub_kubeclient_create_service_account(api_url, namespace: namespace)
+ stub_kubeclient_create_secret(api_url, namespace: namespace)
+
+ stub_kubeclient_get_secret(
+ api_url,
+ {
+ metadata_name: "#{namespace}-token",
+ token: Base64.encode64('sample-token'),
+ namespace: namespace
+ }
+ )
+ end
+ end
+
+ context 'when kubernetes namespace is not persisted' do
+ let(:namespace) { "#{project.path}-#{project.id}" }
+
+ let(:kubernetes_namespace) do
+ build(:cluster_kubernetes_namespace,
+ cluster: cluster,
+ project: cluster_project.project,
+ cluster_project: cluster_project)
+ end
+
+ include_context 'kubernetes requests'
+
+ it 'creates a Clusters::KubernetesNamespace' do
+ expect do
+ subject
+ end.to change(Clusters::KubernetesNamespace, :count).by(1)
+ end
+
+ it 'creates project service account' do
+ expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateServiceAccountService).to receive(:execute).once
+
+ subject
+ end
+
+ it 'configures kubernetes token' do
+ subject
+
+ kubernetes_namespace.reload
+ expect(kubernetes_namespace.namespace).to eq(namespace)
+ expect(kubernetes_namespace.service_account_name).to eq("#{namespace}-service-account")
+ expect(kubernetes_namespace.encrypted_service_account_token).to be_present
+ end
+ end
+
+ context 'when there is a Kubernetes Namespace associated' do
+ let(:namespace) { 'new-namespace' }
+
+ let(:kubernetes_namespace) do
+ create(:cluster_kubernetes_namespace,
+ cluster: cluster,
+ project: cluster_project.project,
+ cluster_project: cluster_project)
+ end
+
+ include_context 'kubernetes requests'
+
+ before do
+ platform.update_column(:namespace, 'new-namespace')
+ end
+
+ it 'does not create any Clusters::KubernetesNamespace' do
+ subject
+
+ expect(cluster.kubernetes_namespace).to eq(kubernetes_namespace)
+ end
+
+ it 'creates project service account' do
+ expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateServiceAccountService).to receive(:execute).once
+
+ subject
+ end
+
+ it 'updates Clusters::KubernetesNamespace' do
+ subject
+
+ kubernetes_namespace.reload
+
+ expect(kubernetes_namespace.namespace).to eq(namespace)
+ expect(kubernetes_namespace.service_account_name).to eq("#{namespace}-service-account")
+ expect(kubernetes_namespace.encrypted_service_account_token).to be_present
+ end
+ end
+end
diff --git a/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb b/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb
index b096f1fa4fb..588edff85d4 100644
--- a/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb
+++ b/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb
@@ -1,94 +1,165 @@
# frozen_string_literal: true
-
require 'spec_helper'
describe Clusters::Gcp::Kubernetes::CreateServiceAccountService do
include KubernetesHelpers
- let(:service) { described_class.new(kubeclient, rbac: rbac) }
+ let(:api_url) { 'http://111.111.111.111' }
+ let(:platform_kubernetes) { cluster.platform_kubernetes }
+ let(:cluster_project) { cluster.cluster_project }
+ let(:project) { cluster_project.project }
+ let(:cluster) do
+ create(:cluster,
+ :project, :provided_by_gcp,
+ platform_kubernetes: create(:cluster_platform_kubernetes, :configured))
+ end
+
+ let(:kubeclient) do
+ Gitlab::Kubernetes::KubeClient.new(
+ api_url,
+ auth_options: { username: 'admin', password: 'xxx' }
+ )
+ end
- describe '#execute' do
- let(:rbac) { false }
- let(:api_url) { 'http://111.111.111.111' }
- let(:username) { 'admin' }
- let(:password) { 'xxx' }
+ shared_examples 'creates service account and token' do
+ it 'creates a kubernetes service account' do
+ subject
+
+ expect(WebMock).to have_requested(:post, api_url + "/api/v1/namespaces/#{namespace}/serviceaccounts").with(
+ body: hash_including(
+ kind: 'ServiceAccount',
+ metadata: { name: service_account_name, namespace: namespace }
+ )
+ )
+ end
- let(:kubeclient) do
- Gitlab::Kubernetes::KubeClient.new(
- api_url,
- auth_options: { username: username, password: password }
+ it 'creates a kubernetes secret' do
+ subject
+
+ expect(WebMock).to have_requested(:post, api_url + "/api/v1/namespaces/#{namespace}/secrets").with(
+ body: hash_including(
+ kind: 'Secret',
+ metadata: {
+ name: token_name,
+ namespace: namespace,
+ annotations: {
+ 'kubernetes.io/service-account.name': service_account_name
+ }
+ },
+ type: 'kubernetes.io/service-account-token'
+ )
)
end
+ end
+
+ before do
+ stub_kubeclient_discover(api_url)
+ stub_kubeclient_get_namespace(api_url, namespace: namespace)
+ stub_kubeclient_create_service_account(api_url, namespace: namespace )
+ stub_kubeclient_create_secret(api_url, namespace: namespace)
+ end
+
+ describe '.gitlab_creator' do
+ let(:namespace) { 'default' }
+ let(:service_account_name) { 'gitlab' }
+ let(:token_name) { 'gitlab-token' }
+
+ subject { described_class.gitlab_creator(kubeclient, rbac: rbac).execute }
+
+ context 'with ABAC cluster' do
+ let(:rbac) { false }
+
+ it_behaves_like 'creates service account and token'
+ end
- subject { service.execute }
+ context 'with RBAC cluster' do
+ let(:rbac) { true }
- context 'when params are correct' do
before do
- stub_kubeclient_discover(api_url)
- stub_kubeclient_create_service_account(api_url)
- stub_kubeclient_create_secret(api_url)
- end
+ cluster.platform_kubernetes.rbac!
- shared_examples 'creates service account and token' do
- it 'creates a kubernetes service account' do
- subject
+ stub_kubeclient_create_cluster_role_binding(api_url)
+ end
- expect(WebMock).to have_requested(:post, api_url + '/api/v1/namespaces/default/serviceaccounts').with(
- body: hash_including(
- kind: 'ServiceAccount',
- metadata: { name: 'gitlab', namespace: 'default' }
- )
- )
- end
-
- it 'creates a kubernetes secret of type ServiceAccountToken' do
- subject
-
- expect(WebMock).to have_requested(:post, api_url + '/api/v1/namespaces/default/secrets').with(
- body: hash_including(
- kind: 'Secret',
- metadata: {
- name: 'gitlab-token',
- namespace: 'default',
- annotations: {
- 'kubernetes.io/service-account.name': 'gitlab'
- }
- },
- type: 'kubernetes.io/service-account-token'
- )
+ it_behaves_like 'creates service account and token'
+
+ it 'should create a cluster role binding with cluster-admin access' do
+ subject
+
+ expect(WebMock).to have_requested(:post, api_url + "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings").with(
+ body: hash_including(
+ kind: 'ClusterRoleBinding',
+ metadata: { name: 'gitlab-admin' },
+ roleRef: {
+ apiGroup: 'rbac.authorization.k8s.io',
+ kind: 'ClusterRole',
+ name: 'cluster-admin'
+ },
+ subjects: [
+ {
+ kind: 'ServiceAccount',
+ name: service_account_name,
+ namespace: namespace
+ }
+ ]
)
- end
+ )
end
+ end
+ end
+
+ describe '.namespace_creator' do
+ let(:namespace) { "#{project.path}-#{project.id}" }
+ let(:service_account_name) { "#{namespace}-service-account" }
+ let(:token_name) { "#{namespace}-token" }
+
+ subject do
+ described_class.namespace_creator(
+ kubeclient,
+ service_account_name: service_account_name,
+ service_account_namespace: namespace,
+ rbac: rbac
+ ).execute
+ end
+
+ context 'with ABAC cluster' do
+ let(:rbac) { false }
+
+ it_behaves_like 'creates service account and token'
+ end
+
+ context 'With RBAC enabled cluster' do
+ let(:rbac) { true }
+
+ before do
+ cluster.platform_kubernetes.rbac!
- context 'abac enabled cluster' do
- it_behaves_like 'creates service account and token'
+ stub_kubeclient_create_role_binding(api_url, namespace: namespace)
end
- context 'rbac enabled cluster' do
- let(:rbac) { true }
-
- before do
- stub_kubeclient_create_cluster_role_binding(api_url)
- end
-
- it_behaves_like 'creates service account and token'
-
- it 'creates a kubernetes cluster role binding' do
- subject
-
- expect(WebMock).to have_requested(:post, api_url + '/apis/rbac.authorization.k8s.io/v1/clusterrolebindings').with(
- body: hash_including(
- kind: 'ClusterRoleBinding',
- metadata: { name: 'gitlab-admin' },
- roleRef: {
- apiGroup: 'rbac.authorization.k8s.io',
- kind: 'ClusterRole',
- name: 'cluster-admin'
- },
- subjects: [{ kind: 'ServiceAccount', namespace: 'default', name: 'gitlab' }]
- )
+ it_behaves_like 'creates service account and token'
+
+ it 'creates a namespaced role binding with edit access' do
+ subject
+
+ expect(WebMock).to have_requested(:post, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings").with(
+ body: hash_including(
+ kind: 'RoleBinding',
+ metadata: { name: "gitlab-#{namespace}", namespace: "#{namespace}" },
+ roleRef: {
+ apiGroup: 'rbac.authorization.k8s.io',
+ kind: 'ClusterRole',
+ name: 'edit'
+ },
+ subjects: [
+ {
+ kind: 'ServiceAccount',
+ name: service_account_name,
+ namespace: namespace
+ }
+ ]
)
- end
+ )
end
end
end
diff --git a/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb b/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb
index 2355827fa5a..a5806559b14 100644
--- a/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb
+++ b/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb
@@ -1,56 +1,59 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
describe Clusters::Gcp::Kubernetes::FetchKubernetesTokenService do
+ include KubernetesHelpers
+
describe '#execute' do
let(:api_url) { 'http://111.111.111.111' }
- let(:username) { 'admin' }
- let(:password) { 'xxx' }
+ let(:namespace) { 'my-namespace' }
+ let(:service_account_token_name) { 'gitlab-token' }
let(:kubeclient) do
Gitlab::Kubernetes::KubeClient.new(
api_url,
- auth_options: { username: username, password: password }
+ auth_options: { username: 'admin', password: 'xxx' }
)
end
- subject { described_class.new(kubeclient).execute }
+ subject { described_class.new(kubeclient, service_account_token_name, namespace).execute }
+
+ before do
+ stub_kubeclient_discover(api_url)
+ end
context 'when params correct' do
let(:decoded_token) { 'xxx.token.xxx' }
let(:token) { Base64.encode64(decoded_token) }
- let(:secret_json) do
- {
- 'metadata': {
- name: 'gitlab-token'
- },
- 'data': {
- 'token': token
- }
- }
- end
-
- before do
- allow_any_instance_of(Kubeclient::Client)
- .to receive(:get_secret).and_return(secret_json)
- end
-
context 'when gitlab-token exists' do
- let(:metadata_name) { 'gitlab-token' }
+ before do
+ stub_kubeclient_get_secret(
+ api_url,
+ {
+ metadata_name: service_account_token_name,
+ namespace: namespace,
+ token: token
+ }
+ )
+ end
it { is_expected.to eq(decoded_token) }
end
- context 'when gitlab-token does not exist' do
- let(:secret_json) { {} }
+ context 'when there is a 500 error' do
+ before do
+ stub_kubeclient_get_secret_error(api_url, service_account_token_name, namespace: namespace, status: 500)
+ end
- it { is_expected.to be_nil }
+ it { expect { subject }.to raise_error(Kubeclient::HttpError) }
end
- context 'when token is nil' do
- let(:token) { nil }
+ context 'when gitlab-token does not exist' do
+ before do
+ stub_kubeclient_get_secret_error(api_url, service_account_token_name, namespace: namespace, status: 404)
+ end
it { is_expected.to be_nil }
end
diff --git a/spec/services/clusters/gcp/provision_service_spec.rb b/spec/services/clusters/gcp/provision_service_spec.rb
index f48afdc83b2..c0bdac40938 100644
--- a/spec/services/clusters/gcp/provision_service_spec.rb
+++ b/spec/services/clusters/gcp/provision_service_spec.rb
@@ -26,7 +26,7 @@ describe Clusters::Gcp::ProvisionService do
end
end
- context 'when suceeded to request provision' do
+ context 'when succeeded to request provision' do
before do
stub_cloud_platform_create_cluster(gcp_project_id, zone)
end
diff --git a/spec/services/clusters/update_service_spec.rb b/spec/services/clusters/update_service_spec.rb
index dcd75b6912d..73f9be242a3 100644
--- a/spec/services/clusters/update_service_spec.rb
+++ b/spec/services/clusters/update_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Clusters::UpdateService do
+ include KubernetesHelpers
+
describe '#execute' do
subject { described_class.new(cluster.user, params).execute(cluster) }
@@ -34,6 +36,11 @@ describe Clusters::UpdateService do
}
end
+ before do
+ allow(ClusterPlatformConfigureWorker).to receive(:perform_async)
+ stub_kubeclient_get_namespace('https://kubernetes.example.com', namespace: 'my-namespace')
+ end
+
it 'updates namespace' do
is_expected.to eq(true)
expect(cluster.platform.namespace).to eq('custom-namespace')
@@ -55,5 +62,32 @@ describe Clusters::UpdateService do
expect(cluster.errors[:"platform_kubernetes.namespace"]).to be_present
end
end
+
+ context 'when cluster is provided by GCP' do
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+
+ let(:params) do
+ {
+ name: 'my-new-name'
+ }
+ end
+
+ it 'does not change cluster name' do
+ is_expected.to eq(false)
+
+ cluster.reload
+ expect(cluster.name).to eq('test-cluster')
+ end
+
+ context 'when cluster is being created' do
+ let(:cluster) { create(:cluster, :providing_by_gcp) }
+
+ it 'rejects changes' do
+ is_expected.to eq(false)
+
+ expect(cluster.errors.full_messages).to include('cannot modify during creation')
+ end
+ end
+ end
end
end
diff --git a/spec/services/commits/commit_patch_service_spec.rb b/spec/services/commits/commit_patch_service_spec.rb
new file mode 100644
index 00000000000..f4fcec2fbc2
--- /dev/null
+++ b/spec/services/commits/commit_patch_service_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Commits::CommitPatchService do
+ describe '#execute' do
+ let(:patches) do
+ patches_folder = Rails.root.join('spec/fixtures/patchfiles')
+ content_1 = File.read(File.join(patches_folder, "0001-This-does-not-apply-to-the-feature-branch.patch"))
+ content_2 = File.read(File.join(patches_folder, "0001-A-commit-from-a-patch.patch"))
+
+ [content_1, content_2]
+ end
+ let(:user) { project.creator }
+ let(:branch_name) { 'branch-with-patches' }
+ let(:project) { create(:project, :repository) }
+ let(:start_branch) { nil }
+ let(:params) { { branch_name: branch_name, patches: patches, start_branch: start_branch } }
+
+ subject(:service) do
+ described_class.new(project, user, params)
+ end
+
+ it 'returns a successful result' do
+ result = service.execute
+
+ branch = project.repository.find_branch(branch_name)
+
+ expect(result[:status]).to eq(:success)
+ expect(result[:result]).to eq(branch.target)
+ end
+
+ it 'is based off HEAD when no start ref is passed' do
+ service.execute
+
+ merge_base = project.repository.merge_base(project.repository.root_ref, branch_name)
+
+ expect(merge_base).to eq(project.repository.commit('HEAD').sha)
+ end
+
+ context 'when specifying a different start branch' do
+ let(:start_branch) { 'with-codeowners' }
+
+ it 'is based of the correct branch' do
+ service.execute
+
+ merge_base = project.repository.merge_base(start_branch, branch_name)
+
+ expect(merge_base).to eq(project.repository.commit(start_branch).sha)
+ end
+ end
+
+ shared_examples 'an error response' do |expected_message|
+ it 'returns the correct error' do
+ result = service.execute
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to match(expected_message)
+ end
+ end
+
+ context 'when the user does not have access' do
+ let(:user) { create(:user) }
+
+ it_behaves_like 'an error response',
+ 'You are not allowed to push into this branch'
+ end
+
+ context 'when the patches are not valid' do
+ let(:patches) { "a" * 2.1.megabytes }
+
+ it_behaves_like 'an error response', 'Patches are too big'
+ end
+
+ context 'when the new branch name is invalid' do
+ let(:branch_name) { 'HEAD' }
+
+ it_behaves_like 'an error response', 'Branch name is invalid'
+ end
+
+ context 'when the patches do not apply' do
+ let(:branch_name) { 'feature' }
+
+ it_behaves_like 'an error response', 'Patch failed at'
+ end
+
+ context 'when specifying a non existent start branch' do
+ let(:start_branch) { 'does-not-exist' }
+
+ it_behaves_like 'an error response', 'Invalid reference name'
+ end
+ end
+end
diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb
deleted file mode 100644
index b9bfbb11511..00000000000
--- a/spec/services/create_deployment_service_spec.rb
+++ /dev/null
@@ -1,335 +0,0 @@
-require 'spec_helper'
-
-describe CreateDeploymentService do
- let(:user) { create(:user) }
- let(:options) { nil }
-
- let(:job) do
- create(:ci_build,
- ref: 'master',
- tag: false,
- environment: 'production',
- options: { environment: options })
- end
-
- let(:project) { job.project }
-
- let!(:environment) do
- create(:environment, project: project, name: 'production')
- end
-
- let(:service) { described_class.new(job) }
-
- before do
- allow_any_instance_of(Deployment).to receive(:create_ref)
- end
-
- describe '#execute' do
- subject { service.execute }
-
- context 'when environment exists' do
- it 'creates a deployment' do
- expect(subject).to be_persisted
- end
- end
-
- context 'when environment does not exist' do
- let(:environment) {}
-
- it 'does not create a deployment' do
- expect do
- expect(subject).to be_nil
- end.not_to change { Deployment.count }
- end
- end
-
- context 'when start action is defined' do
- let(:options) { { action: 'start' } }
-
- context 'and environment is stopped' do
- before do
- environment.stop
- end
-
- it 'makes environment available' do
- subject
-
- expect(environment.reload).to be_available
- end
-
- it 'creates a deployment' do
- expect(subject).to be_persisted
- end
- end
- end
-
- context 'when stop action is defined' do
- let(:options) { { action: 'stop' } }
-
- context 'and environment is available' do
- before do
- environment.start
- end
-
- it 'makes environment stopped' do
- subject
-
- expect(environment.reload).to be_stopped
- end
-
- it 'does not create a deployment' do
- expect(subject).to be_nil
- end
- end
- end
-
- context 'when variables are used' do
- let(:options) do
- { name: 'review-apps/$CI_COMMIT_REF_NAME',
- url: 'http://$CI_COMMIT_REF_NAME.review-apps.gitlab.com' }
- end
-
- before do
- environment.update(name: 'review-apps/master')
- job.update(environment: 'review-apps/$CI_COMMIT_REF_NAME')
- end
-
- it 'creates a new deployment' do
- expect(subject).to be_persisted
- end
-
- it 'does not create a new environment' do
- expect { subject }.not_to change { Environment.count }
- end
-
- it 'updates external url' do
- subject
-
- expect(subject.environment.name).to eq('review-apps/master')
- expect(subject.environment.external_url).to eq('http://master.review-apps.gitlab.com')
- end
- end
-
- context 'when project was removed' do
- let(:environment) {}
-
- before do
- job.update(project: nil)
- end
-
- it 'does not create deployment or environment' do
- expect { subject }.not_to raise_error
-
- expect(Environment.count).to be_zero
- expect(Deployment.count).to be_zero
- end
- end
- end
-
- describe '#expanded_environment_url' do
- subject { service.send(:expanded_environment_url) }
-
- context 'when yaml environment uses $CI_COMMIT_REF_NAME' do
- let(:job) do
- create(:ci_build,
- ref: 'master',
- options: { environment: { url: 'http://review/$CI_COMMIT_REF_NAME' } })
- end
-
- it { is_expected.to eq('http://review/master') }
- end
-
- context 'when yaml environment uses $CI_ENVIRONMENT_SLUG' do
- let(:job) do
- create(:ci_build,
- ref: 'master',
- environment: 'production',
- options: { environment: { url: 'http://review/$CI_ENVIRONMENT_SLUG' } })
- end
-
- let!(:environment) do
- create(:environment,
- project: job.project,
- name: 'production',
- slug: 'prod-slug',
- external_url: 'http://review/old')
- end
-
- it { is_expected.to eq('http://review/prod-slug') }
- end
-
- context 'when yaml environment uses yaml_variables containing symbol keys' do
- let(:job) do
- create(:ci_build,
- yaml_variables: [{ key: :APP_HOST, value: 'host' }],
- options: { environment: { url: 'http://review/$APP_HOST' } })
- end
-
- it { is_expected.to eq('http://review/host') }
- end
-
- context 'when yaml environment does not have url' do
- let(:job) { create(:ci_build, environment: 'staging') }
-
- let!(:environment) do
- create(:environment, project: job.project, name: job.environment)
- end
-
- it 'returns the external_url from persisted environment' do
- is_expected.to be_nil
- end
- end
- end
-
- describe 'processing of builds' do
- shared_examples 'does not create deployment' do
- it 'does not create a new deployment' do
- expect { subject }.not_to change { Deployment.count }
- end
-
- it 'does not call a service' do
- expect_any_instance_of(described_class).not_to receive(:execute)
-
- subject
- end
- end
-
- shared_examples 'creates deployment' do
- it 'creates a new deployment' do
- expect { subject }.to change { Deployment.count }.by(1)
- end
-
- it 'calls a service' do
- expect_any_instance_of(described_class).to receive(:execute)
-
- subject
- end
-
- it 'is set as deployable' do
- subject
-
- expect(Deployment.last.deployable).to eq(deployable)
- end
-
- it 'updates environment URL' do
- subject
-
- expect(Deployment.last.environment.external_url).not_to be_nil
- end
- end
-
- context 'without environment specified' do
- let(:job) { create(:ci_build) }
-
- it_behaves_like 'does not create deployment' do
- subject { job.success }
- end
- end
-
- context 'when environment is specified' do
- let(:deployable) { job }
-
- let(:options) do
- { environment: { name: 'production', url: 'http://gitlab.com' } }
- end
-
- context 'when job succeeds' do
- it_behaves_like 'creates deployment' do
- subject { job.success }
- end
- end
-
- context 'when job fails' do
- it_behaves_like 'does not create deployment' do
- subject { job.drop }
- end
- end
-
- context 'when job is retried' do
- it_behaves_like 'creates deployment' do
- before do
- stub_not_protect_default_branch
-
- project.add_developer(user)
- end
-
- let(:deployable) { Ci::Build.retry(job, user) }
-
- subject { deployable.success }
- end
- end
- end
- end
-
- describe "merge request metrics" do
- let(:merge_request) { create(:merge_request, target_branch: 'master', source_branch: 'feature', source_project: project) }
-
- context "while updating the 'first_deployed_to_production_at' time" do
- before do
- merge_request.metrics.update!(merged_at: Time.now)
- end
-
- context "for merge requests merged before the current deploy" do
- it "sets the time if the deploy's environment is 'production'" do
- time = Time.now
- Timecop.freeze(time) { service.execute }
-
- expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(time)
- end
-
- it "doesn't set the time if the deploy's environment is not 'production'" do
- job.update(environment: 'staging')
- service = described_class.new(job)
- service.execute
-
- expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil
- end
-
- it 'does not raise errors if the merge request does not have a metrics record' do
- merge_request.metrics.destroy
-
- expect(merge_request.reload.metrics).to be_nil
- expect { service.execute }.not_to raise_error
- end
- end
-
- context "for merge requests merged before the previous deploy" do
- context "if the 'first_deployed_to_production_at' time is already set" do
- it "does not overwrite the older 'first_deployed_to_production_at' time" do
- # Previous deploy
- time = Time.now
- Timecop.freeze(time) { service.execute }
-
- expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(time)
-
- # Current deploy
- service = described_class.new(job)
- Timecop.freeze(time + 12.hours) { service.execute }
-
- expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(time)
- end
- end
-
- context "if the 'first_deployed_to_production_at' time is not already set" do
- it "does not overwrite the older 'first_deployed_to_production_at' time" do
- # Previous deploy
- time = 5.minutes.from_now
- Timecop.freeze(time) { service.execute }
-
- expect(merge_request.reload.metrics.merged_at).to be < merge_request.reload.metrics.first_deployed_to_production_at
-
- merge_request.reload.metrics.update(first_deployed_to_production_at: nil)
-
- expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil
-
- # Current deploy
- service = described_class.new(job)
- Timecop.freeze(time + 12.hours) { service.execute }
-
- expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil
- end
- end
- end
- end
- end
-end
diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb
index d71ccfb4334..dd8a1cee074 100644
--- a/spec/services/groups/transfer_service_spec.rb
+++ b/spec/services/groups/transfer_service_spec.rb
@@ -177,7 +177,7 @@ describe Groups::TransferService, :postgresql do
it 'should add an error on group' do
transfer_service.execute(new_parent_group)
- expect(transfer_service.error).to eq('Transfer failed: Validation failed: Path has already been taken')
+ expect(transfer_service.error).to eq('Transfer failed: Validation failed: Group URL has already been taken')
end
end
@@ -347,7 +347,7 @@ describe Groups::TransferService, :postgresql do
end
end
- context 'when transfering a group with nested groups and projects' do
+ context 'when transferring a group with nested groups and projects' do
let!(:group) { create(:group, :public) }
let!(:project1) { create(:project, :repository, :private, namespace: group) }
let!(:subgroup1) { create(:group, :private, parent: group) }
diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb
index 53c85f73cde..f0b0f7956ce 100644
--- a/spec/services/issuable/bulk_update_service_spec.rb
+++ b/spec/services/issuable/bulk_update_service_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Issuable::BulkUpdateService do
let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:project, :repository, namespace: user.namespace) }
def bulk_update(issuables, extra_params = {})
bulk_update_params = extra_params
diff --git a/spec/services/issuable/clone/attributes_rewriter_spec.rb b/spec/services/issuable/clone/attributes_rewriter_spec.rb
new file mode 100644
index 00000000000..20bda6984bd
--- /dev/null
+++ b/spec/services/issuable/clone/attributes_rewriter_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Issuable::Clone::AttributesRewriter do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:project1) { create(:project, :public, group: group) }
+ let(:project2) { create(:project, :public, group: group) }
+ let(:original_issue) { create(:issue, project: project1) }
+ let(:new_issue) { create(:issue, project: project2) }
+
+ subject { described_class.new(user, original_issue, new_issue) }
+
+ context 'setting labels' do
+ it 'sets labels present in the new project and group labels' do
+ project1_label_1 = create(:label, title: 'label1', project: project1)
+ project1_label_2 = create(:label, title: 'label2', project: project1)
+ project2_label_1 = create(:label, title: 'label1', project: project2)
+ group_label = create(:group_label, title: 'group_label', group: group)
+ create(:label, title: 'label3', project: project2)
+
+ original_issue.update(labels: [project1_label_1, project1_label_2, group_label])
+
+ subject.execute
+
+ expect(new_issue.reload.labels).to match_array([project2_label_1, group_label])
+ end
+
+ it 'does not set any labels when not used on the original issue' do
+ subject.execute
+
+ expect(new_issue.reload.labels).to be_empty
+ end
+
+ it 'copies the resource label events' do
+ resource_label_events = create_list(:resource_label_event, 2, issue: original_issue)
+
+ subject.execute
+
+ expected = resource_label_events.map(&:label_id)
+
+ expect(new_issue.resource_label_events.map(&:label_id)).to match_array(expected)
+ end
+ end
+
+ context 'setting milestones' do
+ it 'sets milestone to nil when old issue milestone is not in the new project' do
+ milestone = create(:milestone, title: 'milestone', project: project1)
+
+ original_issue.update(milestone: milestone)
+
+ subject.execute
+
+ expect(new_issue.reload.milestone).to be_nil
+ end
+
+ it 'copies the milestone when old issue milestone title is in the new project' do
+ milestone_project1 = create(:milestone, title: 'milestone', project: project1)
+ milestone_project2 = create(:milestone, title: 'milestone', project: project2)
+
+ original_issue.update(milestone: milestone_project1)
+
+ subject.execute
+
+ expect(new_issue.reload.milestone).to eq(milestone_project2)
+ end
+
+ it 'copies the milestone when old issue milestone is a group milestone' do
+ milestone = create(:milestone, title: 'milestone', group: group)
+
+ original_issue.update(milestone: milestone)
+
+ subject.execute
+
+ expect(new_issue.reload.milestone).to eq(milestone)
+ end
+ end
+end
diff --git a/spec/services/issuable/clone/content_rewriter_spec.rb b/spec/services/issuable/clone/content_rewriter_spec.rb
new file mode 100644
index 00000000000..4d3cb0bd254
--- /dev/null
+++ b/spec/services/issuable/clone/content_rewriter_spec.rb
@@ -0,0 +1,153 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Issuable::Clone::ContentRewriter do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:project1) { create(:project, :public, group: group) }
+ let(:project2) { create(:project, :public, group: group) }
+
+ let(:other_issue) { create(:issue, project: project1) }
+ let(:merge_request) { create(:merge_request) }
+
+ subject { described_class.new(user, original_issue, new_issue)}
+
+ let(:description) { 'Simple text' }
+ let(:original_issue) { create(:issue, description: description, project: project1) }
+ let(:new_issue) { create(:issue, project: project2) }
+
+ context 'rewriting award emojis' do
+ it 'copies the award emojis' do
+ create(:award_emoji, awardable: original_issue, name: 'thumbsup')
+ create(:award_emoji, awardable: original_issue, name: 'thumbsdown')
+
+ expect { subject.execute }.to change { AwardEmoji.count }.by(2)
+
+ expect(new_issue.award_emoji.map(&:name)).to match_array(%w(thumbsup thumbsdown))
+ end
+ end
+
+ context 'rewriting description' do
+ before do
+ subject.execute
+ end
+
+ context 'when description is a simple text' do
+ it 'does not rewrite the description' do
+ expect(new_issue.reload.description).to eq(original_issue.description)
+ end
+ end
+
+ context 'when description contains a local reference' do
+ let(:description) { "See ##{other_issue.iid}" }
+
+ it 'rewrites the local reference correctly' do
+ expected_description = "See #{project1.path}##{other_issue.iid}"
+
+ expect(new_issue.reload.description).to eq(expected_description)
+ end
+ end
+
+ context 'when description contains a cross reference' do
+ let(:description) { "See #{merge_request.project.full_path}!#{merge_request.iid}" }
+
+ it 'rewrites the cross reference correctly' do
+ expected_description = "See #{merge_request.project.full_path}!#{merge_request.iid}"
+
+ expect(new_issue.reload.description).to eq(expected_description)
+ end
+ end
+
+ context 'when description contains a user reference' do
+ let(:description) { "FYU #{user.to_reference}" }
+
+ it 'works with a user reference' do
+ expect(new_issue.reload.description).to eq("FYU #{user.to_reference}")
+ end
+ end
+
+ context 'when description contains uploads' do
+ let(:uploader) { build(:file_uploader, project: project1) }
+ let(:description) { "Text and #{uploader.markdown_link}" }
+
+ it 'rewrites uploads in the description' do
+ upload = Upload.last
+
+ expect(new_issue.description).not_to eq(description)
+ expect(new_issue.description).to match(/Text and #{FileUploader::MARKDOWN_PATTERN}/)
+ expect(upload.secret).not_to eq(uploader.secret)
+ expect(new_issue.description).to include(upload.secret)
+ expect(new_issue.description).to include(upload.path)
+ end
+ end
+ end
+
+ context 'rewriting notes' do
+ context 'simple notes' do
+ let!(:notes) do
+ [
+ create(:note, noteable: original_issue, project: project1,
+ created_at: 2.weeks.ago, updated_at: 1.week.ago),
+ create(:note, noteable: original_issue, project: project1),
+ create(:note, system: true, noteable: original_issue, project: project1)
+ ]
+ end
+ let!(:system_note_metadata) { create(:system_note_metadata, note: notes.last) }
+ let!(:award_emoji) { create(:award_emoji, awardable: notes.first, name: 'thumbsup')}
+
+ before do
+ subject.execute
+ end
+
+ it 'rewrites existing notes in valid order' do
+ expect(new_issue.notes.order('id ASC').pluck(:note).first(3)).to eq(notes.map(&:note))
+ end
+
+ it 'copies all the issue notes' do
+ expect(new_issue.notes.count).to eq(3)
+ end
+
+ it 'does not change the note attributes' do
+ subject.execute
+
+ new_note = new_issue.notes.first
+
+ expect(new_note.note).to eq(notes.first.note)
+ expect(new_note.author).to eq(notes.first.author)
+ end
+
+ it 'copies the award emojis' do
+ subject.execute
+
+ new_note = new_issue.notes.first
+ new_note.award_emoji.first.name = 'thumbsup'
+ end
+
+ it 'copies system_note_metadata for system note' do
+ new_note = new_issue.notes.last
+
+ expect(new_note.system_note_metadata.action).to eq(system_note_metadata.action)
+ expect(new_note.system_note_metadata.id).not_to eq(system_note_metadata.id)
+ end
+ end
+
+ context 'notes with reference' do
+ let(:text) do
+ "See ##{other_issue.iid} and #{merge_request.project.full_path}!#{merge_request.iid}"
+ end
+ let!(:note) { create(:note, noteable: original_issue, note: text, project: project1) }
+
+ it 'rewrites the references correctly' do
+ subject.execute
+
+ new_note = new_issue.notes.first
+
+ expected_text = "See #{other_issue.project.path}##{other_issue.iid} and #{merge_request.project.full_path}!#{merge_request.iid}"
+
+ expect(new_note.note).to eq(expected_text)
+ expect(new_note.author).to eq(note.author)
+ end
+ end
+ end
+end
diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb
index b5767583952..1e088bc7d9b 100644
--- a/spec/services/issues/move_service_spec.rb
+++ b/spec/services/issues/move_service_spec.rb
@@ -10,11 +10,9 @@ describe Issues::MoveService do
let(:sub_group_2) { create(:group, :private, parent: group) }
let(:old_project) { create(:project, namespace: sub_group_1) }
let(:new_project) { create(:project, namespace: sub_group_2) }
- let(:milestone1) { create(:milestone, project_id: old_project.id, title: 'v9.0') }
let(:old_issue) do
- create(:issue, title: title, description: description,
- project: old_project, author: author, milestone: milestone1)
+ create(:issue, title: title, description: description, project: old_project, author: author)
end
subject(:move_service) do
@@ -25,16 +23,6 @@ describe Issues::MoveService do
before do
old_project.add_reporter(user)
new_project.add_reporter(user)
-
- labels = Array.new(2) { |x| "label%d" % (x + 1) }
-
- labels.each do |label|
- old_issue.labels << create(:label,
- project_id: old_project.id,
- title: label)
-
- new_project.labels << create(:label, title: label)
- end
end
end
@@ -48,91 +36,6 @@ describe Issues::MoveService do
context 'issue movable' do
include_context 'user can move issue'
- context 'move to new milestone' do
- let(:new_issue) { move_service.execute(old_issue, new_project) }
-
- context 'project milestone' do
- let!(:milestone2) do
- create(:milestone, project_id: new_project.id, title: 'v9.0')
- end
-
- it 'assigns milestone to new issue' do
- expect(new_issue.reload.milestone.title).to eq 'v9.0'
- expect(new_issue.reload.milestone).to eq(milestone2)
- end
- end
-
- context 'group milestones' do
- let!(:group) { create(:group, :private) }
- let!(:group_milestone_1) do
- create(:milestone, group_id: group.id, title: 'v9.0_group')
- end
-
- before do
- old_issue.update(milestone: group_milestone_1)
- old_project.update(namespace: group)
- new_project.update(namespace: group)
-
- group.add_users([user], GroupMember::DEVELOPER)
- end
-
- context 'when moving to a project of the same group' do
- it 'keeps the same group milestone' do
- expect(new_issue.reload.project).to eq(new_project)
- expect(new_issue.reload.milestone).to eq(group_milestone_1)
- end
- end
-
- context 'when moving to a project of a different group' do
- let!(:group_2) { create(:group, :private) }
-
- let!(:group_milestone_2) do
- create(:milestone, group_id: group_2.id, title: 'v9.0_group')
- end
-
- before do
- old_issue.update(milestone: group_milestone_1)
- new_project.update(namespace: group_2)
-
- group_2.add_users([user], GroupMember::DEVELOPER)
- end
-
- it 'assigns to new group milestone of same title' do
- expect(new_issue.reload.project).to eq(new_project)
- expect(new_issue.reload.milestone).to eq(group_milestone_2)
- end
- end
- end
- end
-
- context 'issue with group labels', :nested_groups do
- it 'assigns group labels to new issue' do
- label = create(:group_label, group: group)
- label_issue = create(:labeled_issue, description: description, project: old_project,
- milestone: milestone1, labels: [label])
- old_project.add_reporter(user)
- new_project.add_reporter(user)
-
- new_issue = move_service.execute(label_issue, new_project)
-
- expect(new_issue).to have_attributes(
- project: new_project,
- labels: include(label)
- )
- end
- end
-
- context 'issue with resource label events' do
- it 'assigns resource label events to new issue' do
- old_issue.resource_label_events = create_list(:resource_label_event, 2, issue: old_issue)
-
- new_issue = move_service.execute(old_issue, new_project)
-
- expected = old_issue.resource_label_events.map(&:label_id)
- expect(new_issue.resource_label_events.map(&:label_id)).to match_array(expected)
- end
- end
-
context 'generic issue' do
include_context 'issue move executed'
@@ -140,18 +43,6 @@ describe Issues::MoveService do
expect(new_issue.project).to eq new_project
end
- it 'assign labels to new issue' do
- expected_label_titles = new_issue.reload.labels.map(&:title)
- expect(expected_label_titles).to include 'label1'
- expect(expected_label_titles).to include 'label2'
- expect(expected_label_titles.size).to eq 2
-
- new_issue.labels.each do |label|
- expect(new_project.labels).to include(label)
- expect(old_project.labels).not_to include(label)
- end
- end
-
it 'rewrites issue title' do
expect(new_issue.title).to eq title
end
@@ -203,140 +94,25 @@ describe Issues::MoveService do
end
end
- context 'issue with notes' do
- context 'notes without references' do
- let(:notes_params) do
- [{ system: false, note: 'Some comment 1' },
- { system: true, note: 'Some system note' },
- { system: false, note: 'Some comment 2' }]
- end
- let(:award_names) { %w(thumbsup thumbsdown facepalm) }
- let(:notes_contents) { notes_params.map { |n| n[:note] } }
-
- before do
- note_params = { noteable: old_issue, project: old_project, author: author }
- notes_params.each_with_index do |note, index|
- new_note = create(:note, note_params.merge(note))
- award_emoji_params = { awardable: new_note, name: award_names[index] }
- create(:award_emoji, award_emoji_params)
- end
- end
-
- include_context 'issue move executed'
-
- let(:all_notes) { new_issue.notes.order('id ASC') }
- let(:system_notes) { all_notes.system }
- let(:user_notes) { all_notes.user }
-
- it 'rewrites existing notes in valid order' do
- expect(all_notes.pluck(:note).first(3)).to eq notes_contents
- end
-
- it 'creates new emojis for the new notes' do
- expect(all_notes.map(&:award_emoji).to_a.flatten.map(&:name)).to eq award_names
- end
-
- it 'adds a system note about move after rewritten notes' do
- expect(system_notes.last.note).to match /^moved from/
- end
-
- it 'preserves orignal author of comment' do
- expect(user_notes.pluck(:author_id)).to all(eq(author.id))
- end
- end
-
- context 'note that has been updated' do
- let!(:note) do
- create(:note, noteable: old_issue, project: old_project,
- author: author, updated_at: Date.yesterday,
- created_at: Date.yesterday)
- end
-
- include_context 'issue move executed'
-
- it 'preserves time when note has been created at' do
- expect(new_issue.notes.first.created_at).to eq note.created_at
- end
+ context 'issue with assignee' do
+ let(:assignee) { create(:user) }
- it 'preserves time when note has been updated at' do
- expect(new_issue.notes.first.updated_at).to eq note.updated_at
- end
- end
-
- context 'issue with assignee' do
- let(:assignee) { create(:user) }
-
- before do
- old_issue.assignees = [assignee]
- end
-
- it 'preserves assignee with access to the new issue' do
- new_project.add_reporter(assignee)
-
- new_issue = move_service.execute(old_issue, new_project)
-
- expect(new_issue.assignees).to eq([assignee])
- end
-
- it 'ignores assignee without access to the new issue' do
- new_issue = move_service.execute(old_issue, new_project)
-
- expect(new_issue.assignees).to be_empty
- end
- end
-
- context 'notes with references' do
- before do
- create(:merge_request, source_project: old_project)
- create(:note, noteable: old_issue, project: old_project, author: author,
- note: 'Note with reference to merge request !1')
- end
-
- include_context 'issue move executed'
- let(:new_note) { new_issue.notes.first }
-
- it 'rewrites references using a cross reference to old project' do
- expect(new_note.note)
- .to eq "Note with reference to merge request #{old_project.to_reference(new_project)}!1"
- end
- end
-
- context 'issue description with uploads' do
- let(:uploader) { build(:file_uploader, project: old_project) }
- let(:description) { "Text and #{uploader.markdown_link}" }
-
- include_context 'issue move executed'
-
- it 'rewrites uploads in description' do
- expect(new_issue.description).not_to eq description
- expect(new_issue.description)
- .to match(/Text and #{FileUploader::MARKDOWN_PATTERN}/)
- expect(new_issue.description).not_to include uploader.secret
- end
+ before do
+ old_issue.assignees = [assignee]
end
- end
- describe 'rewriting references' do
- include_context 'issue move executed'
+ it 'preserves assignee with access to the new issue' do
+ new_project.add_reporter(assignee)
- context 'issue references' do
- let(:another_issue) { create(:issue, project: old_project) }
- let(:description) { "Some description #{another_issue.to_reference}" }
+ new_issue = move_service.execute(old_issue, new_project)
- it 'rewrites referenced issues creating cross project reference' do
- expect(new_issue.description)
- .to eq "Some description #{another_issue.to_reference(new_project)}"
- end
+ expect(new_issue.assignees).to eq([assignee])
end
- context "user references" do
- let(:another_issue) { create(:issue, project: old_project) }
- let(:description) { "Some description #{user.to_reference}" }
+ it 'ignores assignee without access to the new issue' do
+ new_issue = move_service.execute(old_issue, new_project)
- it "doesn't throw any errors for issues containing user references" do
- expect(new_issue.description)
- .to eq "Some description #{user.to_reference}"
- end
+ expect(new_issue.assignees).to be_empty
end
end
@@ -416,25 +192,5 @@ describe Issues::MoveService do
it { expect { move }.to raise_error(StandardError, /permissions/) }
end
end
-
- context 'movable issue with no assigned labels' do
- before do
- old_project.add_reporter(user)
- new_project.add_reporter(user)
-
- labels = Array.new(2) { |x| "label%d" % (x + 1) }
-
- labels.each do |label|
- new_project.labels << create(:label, title: label)
- end
- end
-
- include_context 'issue move executed'
-
- it 'does not assign labels to new issue' do
- expected_label_titles = new_issue.reload.labels.map(&:title)
- expect(expected_label_titles.size).to eq 0
- end
- end
end
end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 07aa8449a66..bd519e7f077 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -343,7 +343,42 @@ describe Issues::UpdateService, :mailer do
end
end
- context 'when the milestone change' do
+ context 'when the milestone is removed' do
+ let!(:non_subscriber) { create(:user) }
+
+ let!(:subscriber) do
+ create(:user) do |u|
+ issue.toggle_subscription(u, project)
+ project.add_developer(u)
+ end
+ end
+
+ it_behaves_like 'system notes for milestones'
+
+ it 'sends notifications for subscribers of changed milestone' do
+ issue.milestone = create(:milestone)
+
+ issue.save
+
+ perform_enqueued_jobs do
+ update_issue(milestone_id: "")
+ end
+
+ should_email(subscriber)
+ should_not_email(non_subscriber)
+ end
+ end
+
+ context 'when the milestone is changed' do
+ let!(:non_subscriber) { create(:user) }
+
+ let!(:subscriber) do
+ create(:user) do |u|
+ issue.toggle_subscription(u, project)
+ project.add_developer(u)
+ end
+ end
+
it 'marks todos as done' do
update_issue(milestone: create(:milestone))
@@ -351,6 +386,15 @@ describe Issues::UpdateService, :mailer do
end
it_behaves_like 'system notes for milestones'
+
+ it 'sends notifications for subscribers of changed milestone' do
+ perform_enqueued_jobs do
+ update_issue(milestone: create(:milestone))
+ end
+
+ should_email(subscriber)
+ should_not_email(non_subscriber)
+ end
end
context 'when the labels change' do
@@ -374,7 +418,7 @@ describe Issues::UpdateService, :mailer do
let!(:non_subscriber) { create(:user) }
let!(:subscriber) do
- create(:user).tap do |u|
+ create(:user) do |u|
label.toggle_subscription(u, project)
project.add_developer(u)
end
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index 9f1da7d9419..c9a668994eb 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -392,5 +392,13 @@ describe MergeRequests::BuildService do
expect(merge_request.source_project).to eq(project)
end
end
+
+ context 'when specifying target branch in the description' do
+ let(:description) { "A merge request targeting another branch\n\n/target_branch with-codeowners" }
+
+ it 'sets the attribute from the quick actions' do
+ expect(merge_request.target_branch).to eq('with-codeowners')
+ end
+ end
end
end
diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb
index 3e33a165e55..274624aa8bb 100644
--- a/spec/services/merge_requests/get_urls_service_spec.rb
+++ b/spec/services/merge_requests/get_urls_service_spec.rb
@@ -6,7 +6,7 @@ describe MergeRequests::GetUrlsService do
let(:project) { create(:project, :public, :repository) }
let(:service) { described_class.new(project) }
let(:source_branch) { "merge-test" }
- let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new/#{source_branch}" }
+ let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{source_branch}" }
let(:show_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/#{merge_request.iid}" }
let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" }
let(:deleted_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 #{Gitlab::Git::BLANK_SHA} refs/heads/#{source_branch}" }
@@ -117,7 +117,7 @@ describe MergeRequests::GetUrlsService do
let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch" }
let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/markdown" }
let(:changes) { "#{new_branch_changes}\n#{existing_branch_changes}" }
- let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new/new_branch" }
+ let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch" }
it 'returns 2 urls for both creating new and showing merge request' do
result = service.execute(changes)
diff --git a/spec/services/merge_requests/reload_diffs_service_spec.rb b/spec/services/merge_requests/reload_diffs_service_spec.rb
index 21f369a3818..5acd01828cb 100644
--- a/spec/services/merge_requests/reload_diffs_service_spec.rb
+++ b/spec/services/merge_requests/reload_diffs_service_spec.rb
@@ -31,35 +31,25 @@ describe MergeRequests::ReloadDiffsService, :use_clean_rails_memory_store_cachin
end
context 'cache clearing' do
- before do
- allow_any_instance_of(Gitlab::Diff::File).to receive(:text?).and_return(true)
- allow_any_instance_of(Gitlab::Diff::File).to receive(:diffable?).and_return(true)
- end
-
- it 'retrieves the diff files to cache the highlighted result' do
- new_diff = merge_request.create_merge_request_diff
- cache_key = new_diff.diffs_collection.cache_key
-
- expect(merge_request).to receive(:create_merge_request_diff).and_return(new_diff)
- expect(Rails.cache).to receive(:read).with(cache_key).and_call_original
- expect(Rails.cache).to receive(:write).with(cache_key, anything, anything).and_call_original
-
- subject.execute
- end
-
it 'clears the cache for older diffs on the merge request' do
old_diff = merge_request.merge_request_diff
old_cache_key = old_diff.diffs_collection.cache_key
- new_diff = merge_request.create_merge_request_diff
- new_cache_key = new_diff.diffs_collection.cache_key
- expect(merge_request).to receive(:create_merge_request_diff).and_return(new_diff)
expect(Rails.cache).to receive(:delete).with(old_cache_key).and_call_original
- expect(Rails.cache).to receive(:read).with(new_cache_key).and_call_original
- expect(Rails.cache).to receive(:write).with(new_cache_key, anything, anything).and_call_original
subject.execute
end
+
+ it 'avoids N+1 queries', :request_store do
+ current_user
+ merge_request
+
+ control_count = ActiveRecord::QueryRecorder.new do
+ subject.execute
+ end.count
+
+ expect { subject.execute }.not_to exceed_query_limit(control_count)
+ end
end
end
end
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 55dfab81c26..be5ad849ba7 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -315,7 +315,42 @@ describe MergeRequests::UpdateService, :mailer do
end
end
- context 'when the milestone change' do
+ context 'when the milestone is removed' do
+ let!(:non_subscriber) { create(:user) }
+
+ let!(:subscriber) do
+ create(:user) do |u|
+ merge_request.toggle_subscription(u, project)
+ project.add_developer(u)
+ end
+ end
+
+ it_behaves_like 'system notes for milestones'
+
+ it 'sends notifications for subscribers of changed milestone' do
+ merge_request.milestone = create(:milestone)
+
+ merge_request.save
+
+ perform_enqueued_jobs do
+ update_merge_request(milestone_id: "")
+ end
+
+ should_email(subscriber)
+ should_not_email(non_subscriber)
+ end
+ end
+
+ context 'when the milestone is changed' do
+ let!(:non_subscriber) { create(:user) }
+
+ let!(:subscriber) do
+ create(:user) do |u|
+ merge_request.toggle_subscription(u, project)
+ project.add_developer(u)
+ end
+ end
+
it 'marks pending todos as done' do
update_merge_request({ milestone: create(:milestone) })
@@ -323,6 +358,15 @@ describe MergeRequests::UpdateService, :mailer do
end
it_behaves_like 'system notes for milestones'
+
+ it 'sends notifications for subscribers of changed milestone' do
+ perform_enqueued_jobs do
+ update_merge_request(milestone: create(:milestone))
+ end
+
+ should_email(subscriber)
+ should_not_email(non_subscriber)
+ end
end
context 'when the labels change' do
@@ -549,8 +593,8 @@ describe MergeRequests::UpdateService, :mailer do
end
context 'setting `allow_collaboration`' do
- let(:target_project) { create(:project, :public) }
- let(:source_project) { fork_project(target_project) }
+ let(:target_project) { create(:project, :repository, :public) }
+ let(:source_project) { fork_project(target_project, nil, repository: true) }
let(:user) { create(:user) }
let(:merge_request) do
create(:merge_request,
diff --git a/spec/services/milestones/destroy_service_spec.rb b/spec/services/milestones/destroy_service_spec.rb
index 8680e428517..9d2be30c636 100644
--- a/spec/services/milestones/destroy_service_spec.rb
+++ b/spec/services/milestones/destroy_service_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Milestones::DestroyService do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:milestone) { create(:milestone, title: 'Milestone v1.0', project: project) }
before do
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index b1290fd0d47..80b015d4cd0 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -57,6 +57,57 @@ describe Notes::CreateService do
end
end
+ context 'noteable highlight cache clearing' do
+ let(:project_with_repo) { create(:project, :repository) }
+ let(:merge_request) do
+ create(:merge_request, source_project: project_with_repo,
+ target_project: project_with_repo)
+ end
+
+ let(:position) do
+ Gitlab::Diff::Position.new(old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 14,
+ diff_refs: merge_request.diff_refs)
+ end
+
+ let(:new_opts) do
+ opts.merge(in_reply_to_discussion_id: nil,
+ type: 'DiffNote',
+ noteable_type: 'MergeRequest',
+ noteable_id: merge_request.id,
+ position: position.to_h)
+ end
+
+ before do
+ allow_any_instance_of(Gitlab::Diff::Position)
+ .to receive(:unfolded_diff?) { true }
+ end
+
+ it 'clears noteable diff cache when it was unfolded for the note position' do
+ expect_any_instance_of(Gitlab::Diff::HighlightCache).to receive(:clear)
+
+ described_class.new(project_with_repo, user, new_opts).execute
+ end
+
+ it 'does not clear cache when note is not the first of the discussion' do
+ prev_note =
+ create(:diff_note_on_merge_request, noteable: merge_request,
+ project: project_with_repo)
+ reply_opts =
+ opts.merge(in_reply_to_discussion_id: prev_note.discussion_id,
+ type: 'DiffNote',
+ noteable_type: 'MergeRequest',
+ noteable_id: merge_request.id,
+ position: position.to_h)
+
+ expect(merge_request).not_to receive(:diffs)
+
+ described_class.new(project_with_repo, user, reply_opts).execute
+ end
+ end
+
context 'note diff file' do
let(:project_with_repo) { create(:project, :repository) }
let(:merge_request) do
diff --git a/spec/services/notes/destroy_service_spec.rb b/spec/services/notes/destroy_service_spec.rb
index 64445be560e..b1f4e87e8ea 100644
--- a/spec/services/notes/destroy_service_spec.rb
+++ b/spec/services/notes/destroy_service_spec.rb
@@ -21,5 +21,38 @@ describe Notes::DestroyService do
expect { described_class.new(project, user).execute(note) }
.to change { user.todos_pending_count }.from(1).to(0)
end
+
+ context 'noteable highlight cache clearing' do
+ let(:repo_project) { create(:project, :repository) }
+ let(:merge_request) do
+ create(:merge_request, source_project: repo_project,
+ target_project: repo_project)
+ end
+
+ let(:note) do
+ create(:diff_note_on_merge_request, project: repo_project,
+ noteable: merge_request)
+ end
+
+ before do
+ allow(note.position).to receive(:unfolded_diff?) { true }
+ end
+
+ it 'clears noteable diff cache when it was unfolded for the note position' do
+ expect(merge_request).to receive_message_chain(:diffs, :clear_cache)
+
+ described_class.new(repo_project, user).execute(note)
+ end
+
+ it 'does not clear cache when note is not the first of the discussion' do
+ reply_note = create(:diff_note_on_merge_request, in_reply_to: note,
+ project: repo_project,
+ noteable: merge_request)
+
+ expect(merge_request).not_to receive(:diffs)
+
+ described_class.new(repo_project, user).execute(reply_note)
+ end
+ end
end
end
diff --git a/spec/services/notes/quick_actions_service_spec.rb b/spec/services/notes/quick_actions_service_spec.rb
index a8c994c101c..14d62763a5b 100644
--- a/spec/services/notes/quick_actions_service_spec.rb
+++ b/spec/services/notes/quick_actions_service_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Notes::QuickActionsService do
shared_context 'note on noteable' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:maintainer) { create(:user).tap { |u| project.add_maintainer(u) } }
let(:assignee) { create(:user) }
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 68a361fa882..2d8da7673dc 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -13,6 +13,54 @@ describe NotificationService, :mailer do
end
end
+ shared_examples 'altered milestone notification on issue' do
+ it 'sends the email to the correct people' do
+ should_email(subscriber_to_new_milestone)
+ issue.assignees.each do |a|
+ should_email(a)
+ end
+ should_email(@u_watcher)
+ should_email(@u_guest_watcher)
+ should_email(@u_participant_mentioned)
+ should_email(@subscriber)
+ should_email(@subscribed_participant)
+ should_email(@watcher_and_subscriber)
+ should_not_email(@u_guest_custom)
+ should_not_email(@u_committer)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(@u_lazy_participant)
+ should_not_email(issue.author)
+ should_not_email(@u_disabled)
+ should_not_email(@u_custom_global)
+ should_not_email(@u_mentioned)
+ end
+ end
+
+ shared_examples 'altered milestone notification on merge request' do
+ it 'sends the email to the correct people' do
+ should_email(subscriber_to_new_milestone)
+ merge_request.assignees.each do |a|
+ should_email(a)
+ end
+ should_email(@u_watcher)
+ should_email(@u_guest_watcher)
+ should_email(@u_participant_mentioned)
+ should_email(@subscriber)
+ should_email(@subscribed_participant)
+ should_email(@watcher_and_subscriber)
+ should_not_email(@u_guest_custom)
+ should_not_email(@u_committer)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(@u_lazy_participant)
+ should_not_email(merge_request.author)
+ should_not_email(@u_disabled)
+ should_not_email(@u_custom_global)
+ should_not_email(@u_mentioned)
+ end
+ end
+
shared_examples 'notifications for new mentions' do
it 'sends no emails when no new mentions are present' do
send_notifications
@@ -952,6 +1000,96 @@ describe NotificationService, :mailer do
end
end
+ describe '#removed_milestone_issue' do
+ it_behaves_like 'altered milestone notification on issue' do
+ let(:milestone) { create(:milestone, project: project, issues: [issue]) }
+ let!(:subscriber_to_new_milestone) { create(:user) { |u| issue.toggle_subscription(u, project) } }
+
+ before do
+ notification.removed_milestone_issue(issue, issue.author)
+ end
+ end
+
+ context 'confidential issues' do
+ let(:author) { create(:user) }
+ let(:assignee) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:member) { create(:user) }
+ let(:guest) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignees: [assignee]) }
+ let(:milestone) { create(:milestone, project: project, issues: [confidential_issue]) }
+
+ it "emails subscribers of the issue's milestone that can read the issue" do
+ project.add_developer(member)
+ project.add_guest(guest)
+
+ confidential_issue.subscribe(non_member, project)
+ confidential_issue.subscribe(author, project)
+ confidential_issue.subscribe(assignee, project)
+ confidential_issue.subscribe(member, project)
+ confidential_issue.subscribe(guest, project)
+ confidential_issue.subscribe(admin, project)
+
+ reset_delivered_emails!
+
+ notification.removed_milestone_issue(confidential_issue, @u_disabled)
+
+ should_not_email(non_member)
+ should_not_email(guest)
+ should_email(author)
+ should_email(assignee)
+ should_email(member)
+ should_email(admin)
+ end
+ end
+ end
+
+ describe '#changed_milestone_issue' do
+ it_behaves_like 'altered milestone notification on issue' do
+ let(:new_milestone) { create(:milestone, project: project, issues: [issue]) }
+ let!(:subscriber_to_new_milestone) { create(:user) { |u| issue.toggle_subscription(u, project) } }
+
+ before do
+ notification.changed_milestone_issue(issue, new_milestone, issue.author)
+ end
+ end
+
+ context 'confidential issues' do
+ let(:author) { create(:user) }
+ let(:assignee) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:member) { create(:user) }
+ let(:guest) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignees: [assignee]) }
+ let(:new_milestone) { create(:milestone, project: project, issues: [confidential_issue]) }
+
+ it "emails subscribers of the issue's milestone that can read the issue" do
+ project.add_developer(member)
+ project.add_guest(guest)
+
+ confidential_issue.subscribe(non_member, project)
+ confidential_issue.subscribe(author, project)
+ confidential_issue.subscribe(assignee, project)
+ confidential_issue.subscribe(member, project)
+ confidential_issue.subscribe(guest, project)
+ confidential_issue.subscribe(admin, project)
+
+ reset_delivered_emails!
+
+ notification.changed_milestone_issue(confidential_issue, new_milestone, @u_disabled)
+
+ should_not_email(non_member)
+ should_not_email(guest)
+ should_email(author)
+ should_email(assignee)
+ should_email(member)
+ should_email(admin)
+ end
+ end
+ end
+
describe '#close_issue' do
before do
update_custom_notification(:close_issue, @u_guest_custom, resource: project)
@@ -1304,6 +1442,28 @@ describe NotificationService, :mailer do
end
end
+ describe '#removed_milestone_merge_request' do
+ it_behaves_like 'altered milestone notification on merge request' do
+ let(:milestone) { create(:milestone, project: project, merge_requests: [merge_request]) }
+ let!(:subscriber_to_new_milestone) { create(:user) { |u| merge_request.toggle_subscription(u, project) } }
+
+ before do
+ notification.removed_milestone_merge_request(merge_request, merge_request.author)
+ end
+ end
+ end
+
+ describe '#changed_milestone_merge_request' do
+ it_behaves_like 'altered milestone notification on merge request' do
+ let(:new_milestone) { create(:milestone, project: project, merge_requests: [merge_request]) }
+ let!(:subscriber_to_new_milestone) { create(:user) { |u| merge_request.toggle_subscription(u, project) } }
+
+ before do
+ notification.changed_milestone_merge_request(merge_request, new_milestone, merge_request.author)
+ end
+ end
+ end
+
describe '#merge_request_unmergeable' do
it "sends email to merge request author" do
notification.merge_request_unmergeable(merge_request)
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index e6ffa2b957b..06f865dc848 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -125,7 +125,7 @@ describe Projects::ImportService do
project.import_type = 'bitbucket'
end
- it 'succeeds if repository import is successfull' 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({})
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 1411723fb9e..2e07d4f8013 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -125,7 +125,7 @@ describe Projects::TransferService do
it { expect(project.errors.messages[:new_namespace].first).to eq 'Please select a new namespace for your project.' }
end
- context 'disallow transfering of project with tags' do
+ context 'disallow transferring of project with tags' do
let(:container_repository) { create(:container_repository) }
before do
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index 41a170e4f25..5a7cafcb60f 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -315,7 +315,7 @@ describe QuickActions::InterpretService do
end
shared_examples 'award command' do
- it 'toggle award 100 emoji if content containts /award :100:' do
+ it 'toggle award 100 emoji if content contains /award :100:' do
_, updates = service.execute(content, issuable)
expect(updates).to eq(emoji_award: "100")
@@ -1213,6 +1213,15 @@ describe QuickActions::InterpretService do
end
end
end
+
+ it 'limits to commands passed ' do
+ content = "/shrug\n/close"
+
+ text, commands = service.execute(content, issue, only: [:shrug])
+
+ expect(commands).to be_empty
+ expect(text).to eq("#{described_class::SHRUG}\n/close")
+ end
end
describe '#explain' do
@@ -1395,7 +1404,7 @@ describe QuickActions::InterpretService do
it 'includes the formatted duration and proper verb' do
_, explanations = service.explain(content, issue)
- expect(explanations).to eq(['Substracts 2h spent time.'])
+ expect(explanations).to eq(['Subtracts 2h spent time.'])
end
end
diff --git a/spec/services/submodules/update_service_spec.rb b/spec/services/submodules/update_service_spec.rb
new file mode 100644
index 00000000000..cf92350c1b2
--- /dev/null
+++ b/spec/services/submodules/update_service_spec.rb
@@ -0,0 +1,212 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Submodules::UpdateService do
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+ let(:user) { create(:user, :commit_email) }
+ let(:branch_name) { project.default_branch }
+ let(:submodule) { 'six' }
+ let(:commit_sha) { 'e25eda1fece24ac7a03624ed1320f82396f35bd8' }
+ let(:commit_message) { 'whatever' }
+ let(:current_sha) { repository.blob_at('HEAD', submodule).id }
+ let(:commit_params) do
+ {
+ submodule: submodule,
+ commit_message: commit_message,
+ commit_sha: commit_sha,
+ branch_name: branch_name
+ }
+ end
+
+ subject { described_class.new(project, user, commit_params) }
+
+ describe "#execute" do
+ shared_examples 'returns error result' do
+ it do
+ result = subject.execute
+
+ expect(result[:status]).to eq :error
+ expect(result[:message]).to eq error_message
+ end
+ end
+
+ context 'when the user is not authorized' do
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'You are not allowed to push into this branch' }
+ end
+ end
+
+ context 'when the user is authorized' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ context 'when the branch is protected' do
+ before do
+ create(:protected_branch, :no_one_can_push, project: project, name: branch_name)
+ end
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'You are not allowed to push into this branch' }
+ end
+ end
+
+ context 'validations' do
+ context 'when submodule' do
+ context 'is empty' do
+ let(:submodule) { '' }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'Invalid parameters' }
+ end
+ end
+
+ context 'is not present' do
+ let(:submodule) { nil }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'Invalid parameters' }
+ end
+ end
+
+ context 'is invalid' do
+ let(:submodule) { 'VERSION' }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'Invalid submodule path' }
+ end
+ end
+
+ context 'does not exist' do
+ let(:submodule) { 'non-existent-submodule' }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'Invalid submodule path' }
+ end
+ end
+
+ context 'has traversal path' do
+ let(:submodule) { '../six' }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'Invalid parameters' }
+ end
+ end
+ end
+
+ context 'commit_sha' do
+ context 'is empty' do
+ let(:commit_sha) { '' }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'Invalid parameters' }
+ end
+ end
+
+ context 'is not present' do
+ let(:commit_sha) { nil }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'Invalid parameters' }
+ end
+ end
+
+ context 'is invalid' do
+ let(:commit_sha) { '1' }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'Invalid parameters' }
+ end
+ end
+
+ context 'is the same as the current ref' do
+ let(:commit_sha) { current_sha }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { "The submodule #{submodule} is already at #{commit_sha}" }
+ end
+ end
+ end
+
+ context 'branch_name' do
+ context 'is empty' do
+ let(:branch_name) { '' }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'You can only create or edit files when you are on a branch' }
+ end
+ end
+
+ context 'is not present' do
+ let(:branch_name) { nil }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'You can only create or edit files when you are on a branch' }
+ end
+ end
+
+ context 'does not exist' do
+ let(:branch_name) { 'non/existent-branch' }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'You can only create or edit files when you are on a branch' }
+ end
+ end
+
+ context 'when commit message is empty' do
+ let(:commit_message) { '' }
+
+ it 'a default commit message is set' do
+ message = "Update submodule #{submodule} with oid #{commit_sha}"
+
+ expect(repository).to receive(:update_submodule).with(any_args, hash_including(message: message))
+
+ subject.execute
+ end
+ end
+ end
+ end
+
+ context 'when there is an unexpected error' do
+ before do
+ allow(repository).to receive(:update_submodule).and_raise(StandardError, 'error message')
+ end
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'error message' }
+ end
+ end
+
+ it 'updates the submodule reference' do
+ result = subject.execute
+
+ expect(result[:status]).to eq :success
+ expect(result[:result]).to eq repository.head_commit.id
+ expect(repository.blob_at('HEAD', submodule).id).to eq commit_sha
+ end
+
+ context 'when submodule is inside a directory' do
+ let(:submodule) { 'test_inside_folder/another_folder/six' }
+ let(:branch_name) { 'submodule_inside_folder' }
+
+ it 'updates the submodule reference' do
+ expect(repository.blob_at(branch_name, submodule).id).not_to eq commit_sha
+
+ subject.execute
+
+ expect(repository.blob_at(branch_name, submodule).id).to eq commit_sha
+ end
+ end
+
+ context 'when repository is empty' do
+ let(:project) { create(:project, :empty_repo) }
+ let(:branch_name) { 'master' }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'The repository is empty' }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 1746721b0d0..c52515aefd8 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -10,7 +10,7 @@ describe TodoService do
let(:john_doe) { create(:user) }
let(:skipped) { create(:user) }
let(:skip_users) { [skipped] }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:mentions) { 'FYI: ' + [author, assignee, john_doe, member, guest, non_member, admin, skipped].map(&:to_reference).join(' ') }
let(:directly_addressed) { [author, assignee, john_doe, member, guest, non_member, admin, skipped].map(&:to_reference).join(' ') }
let(:directly_addressed_and_mentioned) { member.to_reference + ", what do you think? cc: " + [guest, admin, skipped].map(&:to_reference).join(' ') }
diff --git a/spec/services/update_deployment_service_spec.rb b/spec/services/update_deployment_service_spec.rb
new file mode 100644
index 00000000000..3c55dd9659a
--- /dev/null
+++ b/spec/services/update_deployment_service_spec.rb
@@ -0,0 +1,217 @@
+require 'spec_helper'
+
+describe UpdateDeploymentService do
+ let(:user) { create(:user) }
+ let(:options) { { name: 'production' } }
+
+ let(:job) do
+ create(:ci_build,
+ ref: 'master',
+ tag: false,
+ environment: 'production',
+ options: { environment: options },
+ project: project)
+ end
+
+ let(:project) { create(:project, :repository) }
+ let(:environment) { deployment.environment }
+ let(:deployment) { job.deployment }
+ let(:service) { described_class.new(deployment) }
+
+ before do
+ job.success! # Create/Succeed deployment
+ end
+
+ describe '#execute' do
+ subject { service.execute }
+
+ let(:store) { Gitlab::EtagCaching::Store.new }
+
+ it 'invalidates the environment etag cache' do
+ old_value = store.get(environment.etag_cache_key)
+
+ subject
+
+ expect(store.get(environment.etag_cache_key)).not_to eq(old_value)
+ end
+
+ it 'creates ref' do
+ expect_any_instance_of(Repository)
+ .to receive(:create_ref)
+ .with(deployment.ref, deployment.send(:ref_path))
+
+ subject
+ end
+
+ it 'updates merge request metrics' do
+ expect_any_instance_of(Deployment)
+ .to receive(:update_merge_request_metrics!)
+
+ subject
+ end
+
+ context 'when start action is defined' do
+ let(:options) { { name: 'production', action: 'start' } }
+
+ context 'and environment is stopped' do
+ before do
+ environment.stop
+ end
+
+ it 'makes environment available' do
+ subject
+
+ expect(environment.reload).to be_available
+ end
+ end
+ end
+
+ context 'when variables are used' do
+ let(:options) do
+ { name: 'review-apps/$CI_COMMIT_REF_NAME',
+ url: 'http://$CI_COMMIT_REF_NAME.review-apps.gitlab.com' }
+ end
+
+ before do
+ environment.update(name: 'review-apps/master')
+ job.update(environment: 'review-apps/$CI_COMMIT_REF_NAME')
+ end
+
+ it 'does not create a new environment' do
+ expect { subject }.not_to change { Environment.count }
+ end
+
+ it 'updates external url' do
+ subject
+
+ expect(subject.environment.name).to eq('review-apps/master')
+ expect(subject.environment.external_url).to eq('http://master.review-apps.gitlab.com')
+ end
+ end
+ end
+
+ describe '#expanded_environment_url' do
+ subject { service.send(:expanded_environment_url) }
+
+ context 'when yaml environment uses $CI_COMMIT_REF_NAME' do
+ let(:job) do
+ create(:ci_build,
+ ref: 'master',
+ environment: 'production',
+ project: project,
+ options: { environment: { name: 'production', url: 'http://review/$CI_COMMIT_REF_NAME' } })
+ end
+
+ it { is_expected.to eq('http://review/master') }
+ end
+
+ context 'when yaml environment uses $CI_ENVIRONMENT_SLUG' do
+ let(:job) do
+ create(:ci_build,
+ ref: 'master',
+ environment: 'prod-slug',
+ project: project,
+ options: { environment: { name: 'prod-slug', url: 'http://review/$CI_ENVIRONMENT_SLUG' } })
+ end
+
+ it { is_expected.to eq('http://review/prod-slug') }
+ end
+
+ context 'when yaml environment uses yaml_variables containing symbol keys' do
+ let(:job) do
+ create(:ci_build,
+ yaml_variables: [{ key: :APP_HOST, value: 'host' }],
+ environment: 'production',
+ project: project,
+ options: { environment: { name: 'production', url: 'http://review/$APP_HOST' } })
+ end
+
+ it { is_expected.to eq('http://review/host') }
+ end
+
+ context 'when yaml environment does not have url' do
+ let(:job) { create(:ci_build, environment: 'staging', project: project) }
+
+ it 'returns the external_url from persisted environment' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe "merge request metrics" do
+ let(:merge_request) { create(:merge_request, target_branch: 'master', source_branch: 'feature', source_project: project) }
+
+ context "while updating the 'first_deployed_to_production_at' time" do
+ before do
+ merge_request.metrics.update!(merged_at: 1.hour.ago)
+ end
+
+ context "for merge requests merged before the current deploy" do
+ it "sets the time if the deploy's environment is 'production'" do
+ service.execute
+
+ expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(deployment.finished_at)
+ end
+
+ context 'when job deploys to staging' do
+ let(:job) do
+ create(:ci_build,
+ ref: 'master',
+ tag: false,
+ environment: 'staging',
+ options: { environment: { name: 'staging' } },
+ project: project)
+ end
+
+ it "doesn't set the time if the deploy's environment is not 'production'" do
+ service.execute
+
+ expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil
+ end
+ end
+
+ it 'does not raise errors if the merge request does not have a metrics record' do
+ merge_request.metrics.destroy
+
+ expect(merge_request.reload.metrics).to be_nil
+ expect { service.execute }.not_to raise_error
+ end
+ end
+
+ context "for merge requests merged before the previous deploy" do
+ context "if the 'first_deployed_to_production_at' time is already set" do
+ it "does not overwrite the older 'first_deployed_to_production_at' time" do
+ # Previous deploy
+ service.execute
+
+ expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(deployment.finished_at)
+
+ # Current deploy
+ Timecop.travel(12.hours.from_now) do
+ service.execute
+
+ expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(deployment.finished_at)
+ end
+ end
+ end
+
+ context "if the 'first_deployed_to_production_at' time is not already set" do
+ it "does not overwrite the older 'first_deployed_to_production_at' time" do
+ # Previous deploy
+ time = 5.minutes.from_now
+ Timecop.freeze(time) { service.execute }
+
+ expect(merge_request.reload.metrics.merged_at).to be < merge_request.reload.metrics.first_deployed_to_production_at
+
+ previous_time = merge_request.reload.metrics.first_deployed_to_production_at
+
+ # Current deploy
+ Timecop.freeze(time + 12.hours) { service.execute }
+
+ expect(merge_request.reload.metrics.first_deployed_to_production_at).to eq(previous_time)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/users/build_service_spec.rb b/spec/services/users/build_service_spec.rb
index 051e8c87f39..17bc880dec5 100644
--- a/spec/services/users/build_service_spec.rb
+++ b/spec/services/users/build_service_spec.rb
@@ -159,9 +159,9 @@ describe Users::BuildService do
true | true | 'fl@example.com' | '' | true
true | false | 'fl@example.com' | '' | true
- true | nil | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | true
- true | true | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | true
- true | false | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | true
+ true | nil | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false
+ true | true | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false
+ true | false | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false
true | nil | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true
true | true | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index cd69160be10..3fedb9ed48c 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -235,6 +235,10 @@ RSpec.configure do |config|
example.run if Gitlab::Database.mysql?
end
+ config.around(:each, :rails5) do |example|
+ example.run if Gitlab.rails5?
+ end
+
# This makes sure the `ApplicationController#can?` method is stubbed with the
# original implementation for all view specs.
config.before(:each, type: :view) do
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index c0ceb0f6605..18a7a392c12 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -100,7 +100,7 @@ RSpec.configure do |config|
# capybara/rspec already calls Capybara.reset_sessions! in an `after` hook,
# but `block_and_wait_for_requests_complete` is called before it so by
- # calling it explicitely here, we prevent any new requests from being fired
+ # calling it explicitly here, we prevent any new requests from being fired
# See https://github.com/teamcapybara/capybara/blob/ffb41cfad620de1961bb49b1562a9fa9b28c0903/lib/capybara/rspec.rb#L20-L25
# We don't reset the session when the example failed, because we need capybara-screenshot to have access to it.
Capybara.reset_sessions! unless example.exception
diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb
index 80604395adf..18cf08f0b9e 100644
--- a/spec/support/features/discussion_comments_shared_example.rb
+++ b/spec/support/features/discussion_comments_shared_example.rb
@@ -150,17 +150,25 @@ shared_examples 'discussion comments' do |resource_name|
end
if resource_name == 'merge request'
- let(:note_id) { find("#{comments_selector} .note", match: :first)['data-note-id'] }
+ let(:note_id) { find("#{comments_selector} .note:first-child", match: :first)['data-note-id'] }
+ let(:reply_id) { find("#{comments_selector} .note:last-child", match: :first)['data-note-id'] }
it 'shows resolved discussion when toggled' do
+ find("#{comments_selector} .js-vue-discussion-reply").click
+ find("#{comments_selector} .note-textarea").send_keys('a')
+
+ click_button "Comment"
+ wait_for_requests
+
click_button "Resolve discussion"
+ wait_for_requests
expect(page).to have_selector(".note-row-#{note_id}", visible: true)
refresh
- click_button "Toggle discussion"
+ click_button "1 reply"
- expect(page).to have_selector(".note-row-#{note_id}", visible: true)
+ expect(page).to have_selector(".note-row-#{reply_id}", visible: true)
end
end
end
diff --git a/spec/support/features/reportable_note_shared_examples.rb b/spec/support/features/reportable_note_shared_examples.rb
index 89a5518239d..8cfce49da8a 100644
--- a/spec/support/features/reportable_note_shared_examples.rb
+++ b/spec/support/features/reportable_note_shared_examples.rb
@@ -20,7 +20,7 @@ shared_examples 'reportable note' do |type|
dropdown = comment.find(more_actions_selector)
open_dropdown(dropdown)
- expect(dropdown).to have_link('Report as abuse', href: abuse_report_path)
+ expect(dropdown).to have_link('Report abuse to GitLab', href: abuse_report_path)
if type == 'issue' || type == 'merge_request'
expect(dropdown).to have_button('Delete comment')
@@ -33,7 +33,7 @@ shared_examples 'reportable note' do |type|
dropdown = comment.find(more_actions_selector)
open_dropdown(dropdown)
- dropdown.click_link('Report as abuse')
+ dropdown.click_link('Report abuse to GitLab')
expect(find('#user_name')['value']).to match(note.author.username)
expect(find('#abuse_report_message')['value']).to match(noteable_note_url(note))
diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb
index 83035788a56..ecefdc23811 100644
--- a/spec/support/helpers/cycle_analytics_helpers.rb
+++ b/spec/support/helpers/cycle_analytics_helpers.rb
@@ -85,7 +85,7 @@ module CycleAnalyticsHelpers
raise ArgumentError
end
- CreateDeploymentService.new(dummy_job).execute
+ dummy_job.success! # State machine automatically update associated deployment/environment record
end
def dummy_production_job(user, project)
@@ -97,7 +97,7 @@ module CycleAnalyticsHelpers
end
def dummy_pipeline(project)
- Ci::Pipeline.new(
+ create(:ci_pipeline,
sha: project.repository.commit('master').sha,
ref: 'master',
source: :push,
@@ -106,9 +106,7 @@ module CycleAnalyticsHelpers
end
def new_dummy_job(user, project, environment)
- project.environments.find_or_create_by(name: environment)
-
- Ci::Build.new(
+ create(:ci_build,
project: project,
user: user,
environment: environment,
diff --git a/spec/support/helpers/filtered_search_helpers.rb b/spec/support/helpers/filtered_search_helpers.rb
index 5f42ff77fb2..6569feec39b 100644
--- a/spec/support/helpers/filtered_search_helpers.rb
+++ b/spec/support/helpers/filtered_search_helpers.rb
@@ -120,8 +120,12 @@ module FilteredSearchHelpers
create_token('Label', label_name, symbol)
end
- def emoji_token(emoji_name = nil)
- { name: 'My-Reaction', emoji_name: emoji_name }
+ def reaction_token(reaction_name = nil, is_emoji = true)
+ if is_emoji
+ { name: 'My-Reaction', emoji_name: reaction_name }
+ else
+ create_token('My-Reaction', reaction_name)
+ end
end
def default_placeholder
diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb
index a03d9c4045f..ccaf86aa3a6 100644
--- a/spec/support/helpers/kubernetes_helpers.rb
+++ b/spec/support/helpers/kubernetes_helpers.rb
@@ -17,6 +17,7 @@ module KubernetesHelpers
WebMock.stub_request(:get, api_url + '/api/v1').to_return(kube_response(kube_v1_discovery_body))
WebMock.stub_request(:get, api_url + '/apis/extensions/v1beta1').to_return(kube_response(kube_v1beta1_discovery_body))
WebMock.stub_request(:get, api_url + '/apis/rbac.authorization.k8s.io/v1').to_return(kube_response(kube_v1_rbac_authorization_discovery_body))
+ WebMock.stub_request(:get, api_url + '/apis/serving.knative.dev/v1alpha1').to_return(kube_response(kube_v1alpha1_serving_knative_discovery_body))
end
def stub_kubeclient_pods(response = nil)
@@ -41,9 +42,9 @@ module KubernetesHelpers
.to_return(kube_response(kube_v1_secret_body(options)))
end
- def stub_kubeclient_get_secret_error(api_url, name, namespace: 'default')
+ def stub_kubeclient_get_secret_error(api_url, name, namespace: 'default', status: 404)
WebMock.stub_request(:get, api_url + "/api/v1/namespaces/#{namespace}/secrets/#{name}")
- .to_return(status: [404, "Internal Server Error"])
+ .to_return(status: [status, "Internal Server Error"])
end
def stub_kubeclient_create_service_account(api_url, namespace: 'default')
@@ -134,6 +135,18 @@ module KubernetesHelpers
}
end
+ def kube_v1alpha1_serving_knative_discovery_body
+ {
+ "kind" => "APIResourceList",
+ "resources" => [
+ { "name" => "revisions", "namespaced" => true, "kind" => "Revision" },
+ { "name" => "services", "namespaced" => true, "kind" => "Service" },
+ { "name" => "configurations", "namespaced" => true, "kind" => "Configuration" },
+ { "name" => "routes", "namespaced" => true, "kind" => "Route" }
+ ]
+ }
+ end
+
def kube_pods_body
{
"kind" => "PodList",
diff --git a/spec/support/helpers/migrations_helpers.rb b/spec/support/helpers/migrations_helpers.rb
index 0c35764ed9a..5887c3eab74 100644
--- a/spec/support/helpers/migrations_helpers.rb
+++ b/spec/support/helpers/migrations_helpers.rb
@@ -1,6 +1,10 @@
module MigrationsHelpers
+ def active_record_base
+ ActiveRecord::Base
+ end
+
def table(name)
- Class.new(ActiveRecord::Base) do
+ Class.new(active_record_base) do
self.table_name = name
self.inheritance_column = :_type_disabled
@@ -19,7 +23,7 @@ module MigrationsHelpers
end
def clear_schema_cache!
- ActiveRecord::Base.connection_pool.connections.each do |conn|
+ active_record_base.connection_pool.connections.each do |conn|
conn.schema_cache.clear!
end
end
@@ -40,11 +44,18 @@ module MigrationsHelpers
# Reset column information for the most offending classes **after** we
# migrated the schema up, otherwise, column information could be
# outdated. We have a separate method for this so we can override it in EE.
- ActiveRecord::Base.descendants.each(&method(:reset_column_information))
+ active_record_base.descendants.each(&method(:reset_column_information))
+ end
- # Without that, we get errors because of missing attributes, e.g.
+ def refresh_attribute_methods
+ # Without this, we get errors because of missing attributes, e.g.
# super: no superclass method `elasticsearch_indexing' for #<ApplicationSetting:0x00007f85628508d8>
- ApplicationSetting.define_attribute_methods
+ # attr_encrypted also expects ActiveRecord attribute methods to be
+ # defined, or it will override the accessors:
+ # https://gitlab.com/gitlab-org/gitlab-ee/issues/8234#note_113976421
+ [ApplicationSetting, SystemHook].each do |model|
+ model.define_attribute_methods
+ end
end
def reset_column_information(klass)
@@ -84,6 +95,7 @@ module MigrationsHelpers
end
reset_column_in_all_models
+ refresh_attribute_methods
end
def disable_migrations_output
diff --git a/spec/support/helpers/project_forks_helper.rb b/spec/support/helpers/project_forks_helper.rb
index 6a7132c3093..9a86560da2a 100644
--- a/spec/support/helpers/project_forks_helper.rb
+++ b/spec/support/helpers/project_forks_helper.rb
@@ -35,7 +35,7 @@ module ProjectForksHelper
if create_repository
# The call to project.repository.after_import in RepositoryForkWorker does
# not reset the @exists variable of this forked_project.repository
- # so we have to explicitely call this method to clear the @exists variable.
+ # so we have to explicitly call this method to clear the @exists variable.
# of the instance we're returning here.
forked_project.repository.after_import
end
diff --git a/spec/support/helpers/seed_helper.rb b/spec/support/helpers/seed_helper.rb
index 25781f5e679..90d7f60fdeb 100644
--- a/spec/support/helpers/seed_helper.rb
+++ b/spec/support/helpers/seed_helper.rb
@@ -1,15 +1,18 @@
+# frozen_string_literal: true
+
require_relative 'test_env'
# This file is specific to specs in spec/lib/gitlab/git/
SEED_STORAGE_PATH = TestEnv.repos_path
-TEST_REPO_PATH = 'gitlab-git-test.git'.freeze
-TEST_NORMAL_REPO_PATH = 'not-bare-repo.git'.freeze
-TEST_MUTABLE_REPO_PATH = 'mutable-repo.git'.freeze
-TEST_BROKEN_REPO_PATH = 'broken-repo.git'.freeze
+TEST_REPO_PATH = 'gitlab-git-test.git'
+TEST_NORMAL_REPO_PATH = 'not-bare-repo.git'
+TEST_MUTABLE_REPO_PATH = 'mutable-repo.git'
+TEST_BROKEN_REPO_PATH = 'broken-repo.git'
+TEST_GITATTRIBUTES_REPO_PATH = 'with-git-attributes.git'
module SeedHelper
- GITLAB_GIT_TEST_REPO_URL = File.expand_path('../gitlab-git-test.git', __dir__).freeze
+ GITLAB_GIT_TEST_REPO_URL = File.expand_path('../gitlab-git-test.git', __dir__)
def ensure_seeds
if File.exist?(SEED_STORAGE_PATH)
@@ -66,6 +69,11 @@ module SeedHelper
end
def create_git_attributes
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} #{TEST_GITATTRIBUTES_REPO_PATH}),
+ chdir: SEED_STORAGE_PATH,
+ out: '/dev/null',
+ err: '/dev/null')
+
dir = File.join(SEED_STORAGE_PATH, 'with-git-attributes.git', 'info')
FileUtils.mkdir_p(dir)
@@ -82,6 +90,8 @@ foo/bar.* foo
*.cgi key=value?p1=v1&p2=v2
/*.png gitlab-language=png
*.binary binary
+/custom-highlighting/*.gitlab-custom gitlab-language=ruby
+/custom-highlighting/*.gitlab-cgi gitlab-language=erb?parent=json
# This uses a tab instead of spaces to ensure the parser also supports this.
*.md\tgitlab-language=markdown
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index 71d72ff27e9..1f00cdf7e92 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -55,7 +55,11 @@ module TestEnv
'update-gitlab-shell-v-6-0-1' => '2f61d70',
'update-gitlab-shell-v-6-0-3' => 'de78448',
'2-mb-file' => 'bf12d25',
- 'with-codeowners' => '219560e'
+ 'before-create-delete-modify-move' => '845009f',
+ 'between-create-delete-modify-move' => '3f5f443',
+ 'after-create-delete-modify-move' => 'ba3faa7',
+ 'with-codeowners' => '219560e',
+ 'submodule_inside_folder' => 'b491b92'
}.freeze
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
@@ -158,8 +162,9 @@ module TestEnv
version: Gitlab::GitalyClient.expected_server_version,
task: "gitlab:gitaly:install[#{gitaly_dir},#{repos_path}]") do
- start_gitaly(gitaly_dir)
- end
+ Gitlab::SetupHelper.create_gitaly_configuration(gitaly_dir, { 'default' => repos_path }, force: true)
+ start_gitaly(gitaly_dir)
+ end
end
def start_gitaly(gitaly_dir)
diff --git a/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb b/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb
index 9c9d7ad781e..95e69328080 100644
--- a/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb
@@ -34,12 +34,24 @@ shared_examples 'issuable notes filter' do
expect(user.reload.notes_filter_for(issuable)).to eq(0)
end
- it 'returns no system note' do
+ it 'returns only user comments' do
user.set_notes_filter(UserPreference::NOTES_FILTERS[:only_comments], issuable)
get :discussions, namespace_id: project.namespace, project_id: project, id: issuable.iid
+ discussions = JSON.parse(response.body)
- expect(JSON.parse(response.body).count).to eq(1)
+ expect(discussions.count).to eq(1)
+ expect(discussions.first["notes"].first["system"]).to be(false)
+ end
+
+ it 'returns only activity notes' do
+ user.set_notes_filter(UserPreference::NOTES_FILTERS[:only_activity], issuable)
+
+ get :discussions, namespace_id: project.namespace, project_id: project, id: issuable.iid
+ discussions = JSON.parse(response.body)
+
+ expect(discussions.count).to eq(1)
+ expect(discussions.first["notes"].first["system"]).to be(true)
end
context 'when filter is set to "only_comments"' do
diff --git a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
index 9373de5aeab..7038a366144 100644
--- a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
+++ b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
@@ -17,10 +17,10 @@ RSpec.shared_examples 'a creatable merge request' do
sign_in(user)
visit project_new_merge_request_path(
target_project,
- merge_request_source_branch: 'fix',
merge_request: {
source_project_id: source_project.id,
target_project_id: target_project.id,
+ source_branch: 'fix',
target_branch: 'master'
})
end
diff --git a/spec/support/shared_examples/helm_generated_script.rb b/spec/support/shared_examples/helm_generated_script.rb
index ef9bb7f5533..ba9b7d3bdcf 100644
--- a/spec/support/shared_examples/helm_generated_script.rb
+++ b/spec/support/shared_examples/helm_generated_script.rb
@@ -2,18 +2,12 @@ shared_examples 'helm commands' do
describe '#generate_script' do
let(:helm_setup) do
<<~EOS
- set -eo pipefail
- ALPINE_VERSION=$(cat /etc/alpine-release | cut -d '.' -f 1,2)
- echo http://mirror.clarkson.edu/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories
- echo http://mirror1.hs-esslingen.de/pub/Mirrors/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories
- apk add -U wget ca-certificates openssl >/dev/null
- wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.2-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
- mv /tmp/linux-amd64/helm /usr/bin/
+ set -xeo pipefail
EOS
end
it 'should return appropriate command' do
- expect(subject.generate_script).to eq(helm_setup + commands)
+ expect(subject.generate_script.strip).to eq((helm_setup + commands).strip)
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
new file mode 100644
index 00000000000..cf1d52a9616
--- /dev/null
+++ b/spec/support/shared_examples/models/chat_service_spec.rb
@@ -0,0 +1,242 @@
+require "spec_helper"
+
+shared_examples_for "chat service" do |service_name|
+ describe "Associations" do
+ it { is_expected.to belong_to :project }
+ it { is_expected.to have_one :service_hook }
+ end
+
+ describe "Validations" do
+ context "when service is active" do
+ before do
+ subject.active = true
+ end
+
+ it { is_expected.to validate_presence_of(:webhook) }
+ it_behaves_like "issue tracker service URL attribute", :webhook
+ end
+
+ context "when service is inactive" do
+ before do
+ subject.active = false
+ end
+
+ it { is_expected.not_to validate_presence_of(:webhook) }
+ end
+ end
+
+ describe "#execute" do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:webhook_url) { "https://example.gitlab.com/" }
+
+ before do
+ allow(subject).to receive_messages(
+ project: project,
+ project_id: project.id,
+ service_hook: true,
+ webhook: webhook_url
+ )
+
+ WebMock.stub_request(:post, webhook_url)
+ end
+
+ shared_examples "#{service_name} service" do
+ it "calls #{service_name} API" do
+ subject.execute(sample_data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).with { |req| req.body =~ /\A{"#{content_key}":.+}\Z/ }.once
+ end
+ end
+
+ context "with push events" do
+ let(:sample_data) do
+ Gitlab::DataBuilder::Push.build_sample(project, user)
+ end
+
+ it_behaves_like "#{service_name} service"
+
+ it "specifies the webhook when it is configured" do
+ expect(client).to receive(:new).with(client_arguments).and_return(double(:chat_service).as_null_object)
+
+ subject.execute(sample_data)
+ end
+
+ context "with not default branch" do
+ let(:sample_data) do
+ Gitlab::DataBuilder::Push.build(project, user, nil, nil, "not-the-default-branch")
+ end
+
+ context "when notify_only_default_branch enabled" do
+ before do
+ subject.notify_only_default_branch = true
+ end
+
+ it "does not call the Discord Webhooks API" do
+ result = subject.execute(sample_data)
+
+ expect(result).to be_falsy
+ end
+ end
+
+ context "when notify_only_default_branch disabled" do
+ before do
+ subject.notify_only_default_branch = false
+ end
+
+ it_behaves_like "#{service_name} service"
+ end
+ end
+ end
+
+ context "with issue events" do
+ let(:opts) { { title: "Awesome issue", description: "please fix" } }
+ let(:sample_data) do
+ service = Issues::CreateService.new(project, user, opts)
+ issue = service.execute
+ service.hook_data(issue, "open")
+ end
+
+ it_behaves_like "#{service_name} service"
+ end
+
+ context "with merge events" do
+ let(:opts) do
+ {
+ title: "Awesome merge_request",
+ description: "please fix",
+ source_branch: "feature",
+ target_branch: "master"
+ }
+ end
+
+ let(:sample_data) do
+ service = MergeRequests::CreateService.new(project, user, opts)
+ merge_request = service.execute
+ service.hook_data(merge_request, "open")
+ end
+
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like "#{service_name} service"
+ end
+
+ context "with wiki page events" do
+ let(:opts) do
+ {
+ title: "Awesome wiki_page",
+ content: "Some text describing some thing or another",
+ format: "md",
+ message: "user created page: Awesome wiki_page"
+ }
+ end
+ let(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: opts) }
+ let(:sample_data) { Gitlab::DataBuilder::WikiPage.build(wiki_page, user, "create") }
+
+ it_behaves_like "#{service_name} service"
+ end
+
+ context "with note events" do
+ let(:sample_data) { Gitlab::DataBuilder::Note.build(note, user) }
+
+ context "with commit comment" do
+ let(:note) do
+ create(:note_on_commit,
+ author: user,
+ project: project,
+ commit_id: project.repository.commit.id,
+ note: "a comment on a commit")
+ end
+
+ it_behaves_like "#{service_name} service"
+ end
+
+ context "with merge request comment" do
+ let(:note) do
+ create(:note_on_merge_request, project: project, note: "merge request note")
+ end
+
+ it_behaves_like "#{service_name} service"
+ end
+
+ context "with issue comment" do
+ let(:note) do
+ create(:note_on_issue, project: project, note: "issue note")
+ end
+
+ it_behaves_like "#{service_name} service"
+ end
+
+ context "with snippet comment" do
+ let(:note) do
+ create(:note_on_project_snippet, project: project, note: "snippet note")
+ end
+
+ it_behaves_like "#{service_name} service"
+ end
+ end
+
+ context "with pipeline events" do
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project, status: status,
+ sha: project.commit.sha, ref: project.default_branch)
+ end
+ let(:sample_data) { Gitlab::DataBuilder::Pipeline.build(pipeline) }
+
+ context "with failed pipeline" do
+ let(:status) { "failed" }
+
+ it_behaves_like "#{service_name} service"
+ end
+
+ context "with succeeded pipeline" do
+ let(:status) { "success" }
+
+ context "with default notify_only_broken_pipelines" do
+ it "does not call Discord Webhooks API" do
+ result = subject.execute(sample_data)
+
+ expect(result).to be_falsy
+ end
+ end
+
+ context "when notify_only_broken_pipelines is false" do
+ before do
+ subject.notify_only_broken_pipelines = false
+ end
+
+ it_behaves_like "#{service_name} service"
+ end
+ end
+
+ context "with not default branch" do
+ let(:pipeline) do
+ create(:ci_pipeline, project: project, status: "failed", ref: "not-the-default-branch")
+ end
+
+ context "when notify_only_default_branch enabled" do
+ before do
+ subject.notify_only_default_branch = true
+ end
+
+ it "does not call the Discord Webhooks API" do
+ result = subject.execute(sample_data)
+
+ expect(result).to be_falsy
+ end
+ end
+
+ context "when notify_only_default_branch disabled" do
+ before do
+ subject.notify_only_default_branch = false
+ end
+
+ it_behaves_like "#{service_name} service"
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb b/spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb
new file mode 100644
index 00000000000..d87b3181e80
--- /dev/null
+++ b/spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb
@@ -0,0 +1,25 @@
+shared_examples 'cluster application helm specs' do |application_name|
+ let(:application) { create(application_name) }
+
+ describe '#files' do
+ subject { application.files }
+
+ context 'when the helm application does not have a ca_cert' do
+ before do
+ application.cluster.application_helm.ca_cert = nil
+ end
+
+ it 'should not include cert files when there is no ca_cert entry' do
+ expect(subject).not_to include(:'ca.pem', :'cert.pem', :'key.pem')
+ end
+ end
+
+ it 'should include cert files when there is a ca_cert entry' do
+ expect(subject).to include(:'ca.pem', :'cert.pem', :'key.pem')
+ expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert)
+
+ cert = OpenSSL::X509::Certificate.new(subject[:'cert.pem'])
+ expect(cert.not_after).to be < 60.minutes.from_now
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/merge_requests_list.rb b/spec/support/shared_examples/requests/api/merge_requests_list.rb
index 1aed8ab0113..668a390b5d2 100644
--- a/spec/support/shared_examples/requests/api/merge_requests_list.rb
+++ b/spec/support/shared_examples/requests/api/merge_requests_list.rb
@@ -16,7 +16,12 @@ shared_examples 'merge requests list' do
create(:merge_request, state: 'closed', milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: 'Test', created_at: base_time)
- create(:merge_request, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: 'Test', created_at: base_time)
+ merge_request = create(:merge_request, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: 'Test', created_at: base_time)
+
+ merge_request.metrics.update!(merged_by: user,
+ latest_closed_by: user,
+ latest_closed_at: 1.hour.ago,
+ merged_at: 2.hours.ago)
expect do
get api(endpoint_path, user)
diff --git a/spec/support/shared_examples/services/boards/issues_move_service.rb b/spec/support/shared_examples/services/boards/issues_move_service.rb
index 6d29a97c56d..ec44b99d10e 100644
--- a/spec/support/shared_examples/services/boards/issues_move_service.rb
+++ b/spec/support/shared_examples/services/boards/issues_move_service.rb
@@ -34,7 +34,7 @@ shared_examples 'issues move service' do |group|
described_class.new(parent, user, params).execute(issue)
issue.reload
- expect(issue.labels).to contain_exactly(bug)
+ expect(issue.labels).to contain_exactly(bug, regression)
expect(issue).to be_closed
end
end
diff --git a/spec/support/shared_examples/services/boards/lists_move_service.rb b/spec/support/shared_examples/services/boards/lists_move_service.rb
index 07c98cb29b7..2cdb968a45d 100644
--- a/spec/support/shared_examples/services/boards/lists_move_service.rb
+++ b/spec/support/shared_examples/services/boards/lists_move_service.rb
@@ -14,7 +14,7 @@ shared_examples 'lists move service' do
expect(current_list_positions).to eq [0, 1, 2, 3]
end
- it 'keeps position of lists when new positon is equal to old position' do
+ it 'keeps position of lists when new position is equal to old position' do
service = described_class.new(parent, user, position: planning.position)
service.execute(planning)
@@ -22,7 +22,7 @@ shared_examples 'lists move service' do
expect(current_list_positions).to eq [0, 1, 2, 3]
end
- it 'keeps position of lists when new positon is negative' do
+ it 'keeps position of lists when new position is negative' do
service = described_class.new(parent, user, position: -1)
service.execute(planning)
@@ -30,7 +30,7 @@ shared_examples 'lists move service' do
expect(current_list_positions).to eq [0, 1, 2, 3]
end
- it 'keeps position of lists when new positon is equal to number of labels lists' do
+ it 'keeps position of lists when new position is equal to number of labels lists' do
service = described_class.new(parent, user, position: board.lists.label.size)
service.execute(planning)
@@ -38,7 +38,7 @@ shared_examples 'lists move service' do
expect(current_list_positions).to eq [0, 1, 2, 3]
end
- it 'keeps position of lists when new positon is greater than number of labels lists' do
+ it 'keeps position of lists when new position is greater than number of labels lists' do
service = described_class.new(parent, user, position: board.lists.label.size + 1)
service.execute(planning)
@@ -46,7 +46,7 @@ shared_examples 'lists move service' do
expect(current_list_positions).to eq [0, 1, 2, 3]
end
- it 'increments position of intermediate lists when new positon is equal to first position' do
+ it 'increments position of intermediate lists when new position is equal to first position' do
service = described_class.new(parent, user, position: 0)
service.execute(staging)
@@ -54,7 +54,7 @@ shared_examples 'lists move service' do
expect(current_list_positions).to eq [1, 2, 3, 0]
end
- it 'decrements position of intermediate lists when new positon is equal to last position' do
+ it 'decrements position of intermediate lists when new position is equal to last position' do
service = described_class.new(parent, user, position: board.lists.label.last.position)
service.execute(planning)
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 3ba6caf1337..8c4360d4cf0 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -251,7 +251,7 @@ describe 'gitlab:app namespace rake task' do
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
- # Avoid asking gitaly about the root ref (which will fail beacuse of the
+ # Avoid asking gitaly about the root ref (which will fail because of the
# mocked storages)
allow_any_instance_of(Repository).to receive(:empty?).and_return(false)
end
diff --git a/spec/tasks/gitlab/uploads/migrate_rake_spec.rb b/spec/tasks/gitlab/uploads/migrate_rake_spec.rb
index 6fcfae358ec..9588e8be5dc 100644
--- a/spec/tasks/gitlab/uploads/migrate_rake_spec.rb
+++ b/spec/tasks/gitlab/uploads/migrate_rake_spec.rb
@@ -38,14 +38,6 @@ describe 'gitlab:uploads:migrate rake tasks' do
let!(:projects) { create_list(:project, 10, :with_avatar) }
it_behaves_like 'enqueue jobs in batch', batch: 4
-
- context 'Upload has store = nil' do
- before do
- Upload.where(model: projects).update_all(store: nil)
- end
-
- it_behaves_like 'enqueue jobs in batch', batch: 4
- end
end
context "for Group" do
diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb
index 7e24efda5dd..c74e0bf1955 100644
--- a/spec/uploaders/file_uploader_spec.rb
+++ b/spec/uploaders/file_uploader_spec.rb
@@ -81,19 +81,24 @@ describe FileUploader do
end
describe 'copy_to' do
+ let(:new_project) { create(:project) }
+ let(:moved) { described_class.copy_to(subject, new_project) }
+
shared_examples 'returns a valid uploader' do
describe 'returned uploader' do
- let(:new_project) { create(:project) }
- let(:moved) { described_class.copy_to(subject, new_project) }
-
it 'generates a new secret' do
expect(subject).to be
expect(described_class).to receive(:generate_secret).once.and_call_original
expect(moved).to be
end
- it 'create new upload' do
- expect(moved.upload).not_to eq(subject.upload)
+ it 'creates new upload correctly' do
+ upload = moved.upload
+
+ expect(upload).not_to eq(subject.upload)
+ expect(upload.model).to eq(new_project)
+ expect(upload.uploader).to eq('FileUploader')
+ expect(upload.secret).not_to eq(subject.upload.secret)
end
it 'copies the file' do
@@ -111,6 +116,12 @@ describe FileUploader do
end
include_examples 'returns a valid uploader'
+
+ it 'copies the file to the correct location' do
+ expect(moved.upload.path).to eq("#{moved.upload.secret}/dk.png")
+ expect(moved.file.path).to end_with("public/uploads/#{new_project.disk_path}/#{moved.upload.secret}/dk.png")
+ expect(moved.filename).to eq('dk.png')
+ end
end
context 'files are stored remotely' do
@@ -121,6 +132,12 @@ describe FileUploader do
end
include_examples 'returns a valid uploader'
+
+ it 'copies the file to the correct location' do
+ expect(moved.upload.path).to eq("#{new_project.disk_path}/#{moved.upload.secret}/dk.png")
+ expect(moved.file.path).to eq("#{new_project.disk_path}/#{moved.upload.secret}/dk.png")
+ expect(moved.filename).to eq('dk.png')
+ end
end
end
diff --git a/spec/uploaders/namespace_file_uploader_spec.rb b/spec/uploaders/namespace_file_uploader_spec.rb
index 799c6db57fa..d09725ee4be 100644
--- a/spec/uploaders/namespace_file_uploader_spec.rb
+++ b/spec/uploaders/namespace_file_uploader_spec.rb
@@ -55,4 +55,62 @@ describe NamespaceFileUploader do
it_behaves_like "migrates", to_store: described_class::Store::REMOTE
it_behaves_like "migrates", from_store: described_class::Store::REMOTE, to_store: described_class::Store::LOCAL
end
+
+ describe 'copy_to' do
+ let(:group) { create(:group) }
+ let(:moved) { described_class.copy_to(subject, group) }
+
+ shared_examples 'returns a valid uploader' do
+ it 'generates a new secret' do
+ expect(subject).to be
+ expect(described_class).to receive(:generate_secret).once.and_call_original
+ expect(moved).to be
+ end
+
+ it 'creates new upload correctly' do
+ upload = moved.upload
+
+ expect(upload).not_to eq(subject.upload)
+ expect(upload.model).to eq(group)
+ expect(upload.uploader).to eq('NamespaceFileUploader')
+ expect(upload.secret).not_to eq(subject.upload.secret)
+ end
+
+ it 'copies the file' do
+ expect(subject.file).to exist
+ expect(moved.file).to exist
+ expect(subject.file).not_to eq(moved.file)
+ expect(subject.object_store).to eq(moved.object_store)
+ end
+ end
+
+ context 'files are stored locally' do
+ before do
+ subject.store!(fixture_file_upload('spec/fixtures/dk.png'))
+ end
+
+ include_examples 'returns a valid uploader'
+
+ it 'copies the file to the correct location' do
+ expect(moved.upload.path).to eq("#{moved.upload.secret}/dk.png")
+ expect(moved.file.path).to end_with("system/namespace/#{group.id}/#{moved.upload.secret}/dk.png")
+ expect(moved.filename).to eq('dk.png')
+ end
+ end
+
+ context 'files are stored remotely' do
+ before do
+ stub_uploads_object_storage
+ subject.store!(fixture_file_upload('spec/fixtures/dk.png'))
+ subject.migrate!(ObjectStorage::Store::REMOTE)
+ end
+
+ include_examples 'returns a valid uploader'
+
+ it 'copies the file to the correct location' do
+ expect(moved.file.path).to eq("namespace/#{group.id}/#{moved.upload.secret}/dk.png")
+ expect(moved.filename).to eq('dk.png')
+ end
+ end
+ end
end
diff --git a/spec/views/projects/blob/_viewer.html.haml_spec.rb b/spec/views/projects/blob/_viewer.html.haml_spec.rb
index aedbaa66d34..95f7f87d37b 100644
--- a/spec/views/projects/blob/_viewer.html.haml_spec.rb
+++ b/spec/views/projects/blob/_viewer.html.haml_spec.rb
@@ -29,6 +29,8 @@ describe 'projects/blob/_viewer.html.haml' do
controller.params[:namespace_id] = project.namespace.to_param
controller.params[:project_id] = project.to_param
controller.params[:id] = File.join('master', blob.path)
+
+ allow(project.repository).to receive(:gitattribute).and_return(nil)
end
def render_view
diff --git a/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb b/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb
index 9c0be249a50..8a9ab02eaca 100644
--- a/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb
+++ b/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb
@@ -12,10 +12,10 @@ describe 'projects/notes/_more_actions_dropdown' do
assign(:project, project)
end
- it 'shows Report as abuse button if not editable and not current users comment' do
+ it 'shows Report abuse to GitLab button if not editable and not current users comment' do
render 'projects/notes/more_actions_dropdown', current_user: not_author_user, note_editable: false, note: note
- expect(rendered).to have_link('Report as abuse')
+ expect(rendered).to have_link('Report abuse to GitLab')
end
it 'does not show the More actions button if not editable and current users comment' do
@@ -24,10 +24,10 @@ describe 'projects/notes/_more_actions_dropdown' do
expect(rendered).not_to have_selector('.dropdown.more-actions')
end
- it 'shows Report as abuse and Delete buttons if editable and not current users comment' do
+ it 'shows Report abuse to GitLab and Delete buttons if editable and not current users comment' do
render 'projects/notes/more_actions_dropdown', current_user: not_author_user, note_editable: true, note: note
- expect(rendered).to have_link('Report as abuse')
+ expect(rendered).to have_link('Report abuse to GitLab')
expect(rendered).to have_link('Delete comment')
end
diff --git a/spec/views/projects/tree/_blob_item.html.haml_spec.rb b/spec/views/projects/tree/_tree_row.html.haml_spec.rb
index 6a477c712ff..3353b7665e2 100644
--- a/spec/views/projects/tree/_blob_item.html.haml_spec.rb
+++ b/spec/views/projects/tree/_tree_row.html.haml_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'projects/tree/_blob_item' do
+describe 'projects/tree/_tree_row' do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:blob_item) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID, 'files/ruby').first }
@@ -31,10 +31,7 @@ describe 'projects/tree/_blob_item' do
end
end
- def render_partial(blob_item)
- render partial: 'projects/tree/blob_item', locals: {
- blob_item: blob_item,
- type: 'blob'
- }
+ def render_partial(items)
+ render partial: 'projects/tree/tree_row', collection: [items].flatten
end
end
diff --git a/spec/workers/build_success_worker_spec.rb b/spec/workers/build_success_worker_spec.rb
index dba70883130..5eb9709ded9 100644
--- a/spec/workers/build_success_worker_spec.rb
+++ b/spec/workers/build_success_worker_spec.rb
@@ -2,15 +2,39 @@ require 'spec_helper'
describe BuildSuccessWorker do
describe '#perform' do
+ subject { described_class.new.perform(build.id) }
+
+ before do
+ allow_any_instance_of(Deployment).to receive(:create_ref)
+ end
+
context 'when build exists' do
- context 'when build belogs to the environment' do
- let!(:build) { create(:ci_build, environment: 'production') }
+ context 'when deployment was not created with the build creation' do # An edge case during the transition period
+ let!(:build) { create(:ci_build, :deploy_to_production) }
+
+ before do
+ Deployment.delete_all
+ build.reload
+ end
- it 'executes deployment service' do
- expect_any_instance_of(CreateDeploymentService)
- .to receive(:execute)
+ it 'creates a successful deployment' do
+ expect(build).not_to be_has_deployment
- described_class.new.perform(build.id)
+ subject
+
+ build.reload
+ expect(build).to be_has_deployment
+ expect(build.deployment).to be_success
+ end
+ end
+
+ context 'when deployment was created with the build creation' do # Counter part of the above edge case
+ let!(:build) { create(:ci_build, :deploy_to_production) }
+
+ it 'does not create a new deployment' do
+ expect(build).to be_has_deployment
+
+ expect { subject }.not_to change { Deployment.count }
end
end
@@ -18,10 +42,22 @@ describe BuildSuccessWorker do
let!(:build) { create(:ci_build, project: nil) }
it 'does not create deployment' do
- expect_any_instance_of(CreateDeploymentService)
- .not_to receive(:execute)
+ subject
+
+ expect(build.reload).not_to be_has_deployment
+ end
+ end
+
+ context 'when the build will stop an environment' do
+ let!(:build) { create(:ci_build, :stop_review_app, environment: environment.name, project: environment.project) }
+ let(:environment) { create(:environment, state: :available) }
+
+ it 'stops the environment' do
+ expect(environment).to be_available
+
+ subject
- described_class.new.perform(build.id)
+ expect(environment.reload).to be_stopped
end
end
end
diff --git a/spec/workers/cluster_platform_configure_worker_spec.rb b/spec/workers/cluster_platform_configure_worker_spec.rb
new file mode 100644
index 00000000000..b51f6e07c6a
--- /dev/null
+++ b/spec/workers/cluster_platform_configure_worker_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ClusterPlatformConfigureWorker, '#execute' do
+ 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)
+
+ described_class.new.perform(cluster.id)
+ end
+ end
+
+ context 'when provider type is user' do
+ let(:cluster) { create(:cluster, :project, :provided_by_user) }
+
+ it 'configures kubernetes platform' do
+ expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute)
+
+ described_class.new.perform(cluster.id)
+ end
+ end
+
+ context 'when cluster does not exist' do
+ it 'does not provision a cluster' do
+ expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).not_to receive(:execute)
+
+ described_class.new.perform(123)
+ end
+ end
+
+ context 'when kubeclient raises error' do
+ let(:cluster) { create(:cluster, :project) }
+
+ it 'rescues and logs the error' do
+ allow_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute).and_raise(::Kubeclient::HttpError.new(500, 'something baaaad happened', ''))
+
+ expect(Rails.logger)
+ .to receive(:error)
+ .with("Failed to create/update Kubernetes namespace for cluster_id: #{cluster.id} with error: something baaaad happened")
+
+ described_class.new.perform(cluster.id)
+ end
+ end
+end
diff --git a/spec/workers/cluster_provision_worker_spec.rb b/spec/workers/cluster_provision_worker_spec.rb
index 8054ec11a48..0a2dfef36a4 100644
--- a/spec/workers/cluster_provision_worker_spec.rb
+++ b/spec/workers/cluster_provision_worker_spec.rb
@@ -14,18 +14,25 @@ describe ClusterProvisionWorker do
end
context 'when provider type is user' do
- let(:cluster) { create(:cluster, provider_type: :user) }
+ let(:cluster) { create(:cluster, :provided_by_user) }
it 'does not provision a cluster' do
expect_any_instance_of(Clusters::Gcp::ProvisionService).not_to receive(:execute)
described_class.new.perform(cluster.id)
end
+
+ it 'configures kubernetes platform' do
+ expect(ClusterPlatformConfigureWorker).to receive(:perform_async).with(cluster.id)
+
+ described_class.new.perform(cluster.id)
+ end
end
context 'when cluster does not exist' do
it 'does not provision a cluster' do
expect_any_instance_of(Clusters::Gcp::ProvisionService).not_to receive(:execute)
+ expect(ClusterPlatformConfigureWorker).not_to receive(:perform_async)
described_class.new.perform(123)
end
diff --git a/spec/workers/deployments/success_worker_spec.rb b/spec/workers/deployments/success_worker_spec.rb
new file mode 100644
index 00000000000..ba7d45eca01
--- /dev/null
+++ b/spec/workers/deployments/success_worker_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe Deployments::SuccessWorker do
+ subject { described_class.new.perform(deployment&.id) }
+
+ context 'when successful deployment' do
+ let(:deployment) { create(:deployment, :success) }
+
+ it 'executes UpdateDeploymentService' do
+ expect(UpdateDeploymentService)
+ .to receive(:new).with(deployment).and_call_original
+
+ subject
+ end
+ end
+
+ context 'when canceled deployment' do
+ let(:deployment) { create(:deployment, :canceled) }
+
+ it 'does not execute UpdateDeploymentService' do
+ expect(UpdateDeploymentService).not_to receive(:new)
+
+ subject
+ end
+ end
+
+ context 'when deploy record does not exist' do
+ let(:deployment) { nil }
+
+ it 'does not execute UpdateDeploymentService' do
+ expect(UpdateDeploymentService).not_to receive(:new)
+
+ subject
+ end
+ end
+end
diff --git a/spec/workers/email_receiver_worker_spec.rb b/spec/workers/email_receiver_worker_spec.rb
index e4e77c667b3..045135255d6 100644
--- a/spec/workers/email_receiver_worker_spec.rb
+++ b/spec/workers/email_receiver_worker_spec.rb
@@ -46,6 +46,21 @@ describe EmailReceiverWorker, :mailer do
should_not_email_anyone
end
end
+
+ context 'when the error is Gitlab::Email::InvalidAttachment' do
+ let(:error) { Gitlab::Email::InvalidAttachment.new("Could not deal with that") }
+
+ it 'reports the error to the sender' do
+ perform_enqueued_jobs do
+ described_class.new.perform(raw_message)
+ end
+
+ email = ActionMailer::Base.deliveries.last
+ expect(email).not_to be_nil
+ expect(email.to).to eq(["jake@adventuretime.ooo"])
+ expect(email.body.parts.last.to_s).to include("Could not deal with that")
+ end
+ end
end
end
diff --git a/spec/workers/stuck_import_jobs_worker_spec.rb b/spec/workers/stuck_import_jobs_worker_spec.rb
index 2169c14218b..e94d2be9850 100644
--- a/spec/workers/stuck_import_jobs_worker_spec.rb
+++ b/spec/workers/stuck_import_jobs_worker_spec.rb
@@ -8,29 +8,29 @@ describe StuckImportJobsWorker do
context 'when the import status was already updated' do
before do
allow(Gitlab::SidekiqStatus).to receive(:completed_jids) do
- project.import_start
- project.import_finish
+ import_state.start
+ import_state.finish
- [project.import_jid]
+ [import_state.jid]
end
end
it 'does not mark the project as failed' do
worker.perform
- expect(project.reload.import_status).to eq('finished')
+ expect(import_state.reload.status).to eq('finished')
end
end
context 'when the import status was not updated' do
before do
- allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return([project.import_jid])
+ allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return([import_state.jid])
end
it 'marks the project as failed' do
worker.perform
- expect(project.reload.import_status).to eq('failed')
+ expect(import_state.reload.status).to eq('failed')
end
end
end
@@ -41,27 +41,27 @@ describe StuckImportJobsWorker do
end
it 'does not mark the project as failed' do
- expect { worker.perform }.not_to change { project.reload.import_status }
+ expect { worker.perform }.not_to change { import_state.reload.status }
end
end
end
describe 'with scheduled import_status' do
it_behaves_like 'project import job detection' do
- let(:project) { create(:project, :import_scheduled) }
+ let(:import_state) { create(:project, :import_scheduled).import_state }
before do
- project.import_state.update(jid: '123')
+ import_state.update(jid: '123')
end
end
end
describe 'with started import_status' do
it_behaves_like 'project import job detection' do
- let(:project) { create(:project, :import_started) }
+ let(:import_state) { create(:project, :import_started).import_state }
before do
- project.import_state.update(jid: '123')
+ import_state.update(jid: '123')
end
end
end
diff --git a/vendor/gitignore/Android.gitignore b/vendor/gitignore/Android.gitignore
index 39b6783cef8..69eda01429a 100644
--- a/vendor/gitignore/Android.gitignore
+++ b/vendor/gitignore/Android.gitignore
@@ -1,6 +1,7 @@
# Built application files
*.apk
*.ap_
+*.aab
# Files for the ART/Dalvik VM
*.dex
@@ -43,8 +44,9 @@ captures/
.idea/caches
# Keystore files
-# Uncomment the following line if you do not want to check your keystore files in.
+# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
+#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
diff --git a/vendor/gitignore/Delphi.gitignore b/vendor/gitignore/Delphi.gitignore
index 000ee5f104b..9532800ba22 100644
--- a/vendor/gitignore/Delphi.gitignore
+++ b/vendor/gitignore/Delphi.gitignore
@@ -64,3 +64,6 @@ __recovery/
# Castalia statistics file (since XE7 Castalia is distributed with Delphi)
*.stat
+
+# Boss dependency manager vendor folder https://github.com/HashLoad/boss
+modules/
diff --git a/vendor/gitignore/Elixir.gitignore b/vendor/gitignore/Elixir.gitignore
index 86e4c3f3905..b263cd10f37 100644
--- a/vendor/gitignore/Elixir.gitignore
+++ b/vendor/gitignore/Elixir.gitignore
@@ -7,3 +7,4 @@ erl_crash.dump
*.ez
*.beam
/config/*.secret.exs
+.elixir_ls/
diff --git a/vendor/gitignore/Global/Images.gitignore b/vendor/gitignore/Global/Images.gitignore
new file mode 100644
index 00000000000..97dcdbe6a95
--- /dev/null
+++ b/vendor/gitignore/Global/Images.gitignore
@@ -0,0 +1,63 @@
+# JPEG
+*.jpg
+*.jpeg
+*.jpe
+*.jif
+*.jfif
+*.jfi
+
+# JPEG 2000
+*.jp2
+*.j2k
+*.jpf
+*.jpx
+*.jpm
+*.mj2
+
+# JPEG XR
+*.jxr
+*.hdp
+*.wdp
+
+# Graphics Interchange Format
+*.gif
+
+# RAW
+*.raw
+
+# Web P
+*.webp
+
+# Portable Network Graphics
+*.png
+
+# Animated Portable Network Graphics
+*.apng
+
+# Multiple-image Network Graphics
+*.mng
+
+# Tagged Image File Format
+*.tiff
+*.tif
+
+# Scalable Vector Graphics
+*.svg
+*.svgz
+
+# Portable Document Format
+*.pdf
+
+# X BitMap
+*.xbm
+
+# BMP
+*.bmp
+*.dib
+
+# ICO
+*.ico
+
+# 3D Images
+*.3dm
+*.max
diff --git a/vendor/gitignore/Global/NetBeans.gitignore b/vendor/gitignore/Global/NetBeans.gitignore
index 254108cd23b..863bc7fa66e 100644
--- a/vendor/gitignore/Global/NetBeans.gitignore
+++ b/vendor/gitignore/Global/NetBeans.gitignore
@@ -1,4 +1,4 @@
-nbproject/private/
+**/nbproject/private/
build/
nbbuild/
dist/
diff --git a/vendor/gitignore/Global/PSoCCreator.gitignore b/vendor/gitignore/Global/PSoCCreator.gitignore
new file mode 100644
index 00000000000..15ae040bcda
--- /dev/null
+++ b/vendor/gitignore/Global/PSoCCreator.gitignore
@@ -0,0 +1,18 @@
+# Project Settings
+*.cywrk.*
+*.cyprj.*
+
+# Generated Assets and Resources
+Debug/
+Release/
+Export/
+*/codegentemp
+*/Generated_Source
+*_datasheet.pdf
+*_timing.html
+*.cycdx
+*.cyfit
+*.rpt
+*.svd
+*.log
+*.zip
diff --git a/vendor/gitignore/Global/Xcode.gitignore b/vendor/gitignore/Global/Xcode.gitignore
index cd0c7d3e45a..b01314d3a64 100644
--- a/vendor/gitignore/Global/Xcode.gitignore
+++ b/vendor/gitignore/Global/Xcode.gitignore
@@ -2,17 +2,11 @@
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
-## User settings
-xcuserdata/
-
-## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
-*.xcscmblueprint
-*.xccheckout
-
-## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
+## Build generated
build/
DerivedData/
-*.moved-aside
+
+## Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
@@ -21,3 +15,65 @@ DerivedData/
!default.mode2v3
*.perspectivev3
!default.perspectivev3
+xcuserdata/
+
+## Other
+*.moved-aside
+*.xccheckout
+*.xcscmblueprint
+
+## Obj-C/Swift specific
+*.hmap
+*.ipa
+*.dSYM.zip
+*.dSYM
+
+## Playgrounds
+timeline.xctimeline
+playground.xcworkspace
+
+# Swift Package Manager
+#
+# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
+# Packages/
+# Package.pins
+# Package.resolved
+.build/
+
+# CocoaPods
+#
+# We recommend against adding the Pods directory to your .gitignore. However
+# you should judge for yourself, the pros and cons are mentioned at:
+# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
+#
+# Pods/
+#
+# Add this line if you want to avoid checking in source code from the Xcode workspace
+# *.xcworkspace
+
+# Carthage
+#
+# Add this line if you want to avoid checking in source code from Carthage dependencies.
+# Carthage/Checkouts
+
+Carthage/Build
+
+# fastlane
+#
+# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
+# screenshots whenever they are needed.
+# For more information about the recommended setup visit:
+# https://docs.fastlane.tools/best-practices/source-control/#source-control
+
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots/**/*.png
+fastlane/test_output
+
+# Code Injection
+#
+# After new code Injection tools there's a generated folder /iOSInjectionProject
+# https://github.com/johnno1962/injectionforxcode
+
+iOSInjectionProject/
+
diff --git a/vendor/gitignore/Laravel.gitignore b/vendor/gitignore/Laravel.gitignore
index 67e2146f2bc..6552ddf8a06 100644
--- a/vendor/gitignore/Laravel.gitignore
+++ b/vendor/gitignore/Laravel.gitignore
@@ -1,4 +1,4 @@
-vendor/
+/vendor/
node_modules/
npm-debug.log
yarn-error.log
diff --git a/vendor/gitignore/Magento.gitignore b/vendor/gitignore/Magento.gitignore
index 6f1fa223992..abe6d79fedb 100644
--- a/vendor/gitignore/Magento.gitignore
+++ b/vendor/gitignore/Magento.gitignore
@@ -2,6 +2,8 @@
# Magento Default Files #
#--------------------------#
+/PATCH_*.sh
+
/app/etc/local.xml
/media/*
diff --git a/vendor/gitignore/Node.gitignore b/vendor/gitignore/Node.gitignore
index c221276ebae..e1da6ae8ea5 100644
--- a/vendor/gitignore/Node.gitignore
+++ b/vendor/gitignore/Node.gitignore
@@ -71,3 +71,6 @@ typings/
# Serverless directories
.serverless
+
+# FuseBox cache
+.fusebox/
diff --git a/vendor/gitignore/Python.gitignore b/vendor/gitignore/Python.gitignore
index 6f7a6d9c3d7..510c73d0fdb 100644
--- a/vendor/gitignore/Python.gitignore
+++ b/vendor/gitignore/Python.gitignore
@@ -109,3 +109,6 @@ venv.bak/
.mypy_cache/
.dmypy.json
dmypy.json
+
+# Pyre type checker
+.pyre/
diff --git a/vendor/gitignore/Rails.gitignore b/vendor/gitignore/Rails.gitignore
index 78eb74fdc26..38ba1b5b38c 100644
--- a/vendor/gitignore/Rails.gitignore
+++ b/vendor/gitignore/Rails.gitignore
@@ -50,6 +50,7 @@ node_modules/
# Ignore precompiled javascript packs
/public/packs
/public/packs-test
+/public/assets
# Ignore yarn files
/yarn-error.log
diff --git a/vendor/gitignore/Unity.gitignore b/vendor/gitignore/Unity.gitignore
index 0210746b1a5..833e6d4291c 100644
--- a/vendor/gitignore/Unity.gitignore
+++ b/vendor/gitignore/Unity.gitignore
@@ -23,6 +23,7 @@ ExportedObj/
*.svd
*.pdb
*.opendb
+*.VC.db
# Unity3D generated meta files
*.pidb.meta
diff --git a/vendor/licenses.csv b/vendor/licenses.csv
index 9083fe076c3..ea3d3fd02f9 100644
--- a/vendor/licenses.csv
+++ b/vendor/licenses.csv
@@ -67,8 +67,9 @@
@babel/template,7.1.2,MIT
@babel/traverse,7.1.0,MIT
@babel/types,7.1.2,MIT
-@gitlab-org/gitlab-svgs,1.31.0,MIT
-@gitlab-org/gitlab-ui,1.8.0,MIT
+@gitlab-org/gitlab-svgs,1.32.0,MIT
+@gitlab-org/gitlab-ui,1.10.0,MIT
+@gitlab/svgs,1.35.0,MIT
@sindresorhus/is,0.7.0,MIT
@types/jquery,2.0.48,MIT
@vue/component-compiler-utils,2.2.0,MIT
@@ -133,7 +134,6 @@ asciidoctor,1.5.6.2,MIT
asciidoctor-plantuml,0.0.8,MIT
asn1.js,4.10.1,MIT
assert,1.4.1,MIT
-asset_sync,2.4.0,MIT
assign-symbols,1.0.0,MIT
async-each,1.0.1,MIT
async-limiter,1.0.0,MIT
@@ -161,7 +161,6 @@ big.js,3.2.0,MIT
binary-extensions,1.11.0,MIT
binaryextensions,2.1.1,MIT
bindata,2.4.3,ruby
-blackst0ne-mermaid,7.1.0-fixed,MIT
bluebird,3.5.1,MIT
bn.js,4.11.8,MIT
body-parser,1.18.2,MIT
@@ -264,8 +263,8 @@ css-selector-tokenizer,0.7.0,MIT
css_parser,1.5.0,MIT
cssesc,0.1.0,MIT
cyclist,0.2.2,MIT*
-d3,3.5.17,New BSD
d3,4.12.2,New BSD
+d3,4.13.0,New BSD
d3-array,1.2.1,New BSD
d3-axis,1.0.8,New BSD
d3-brush,1.0.4,New BSD
@@ -278,6 +277,7 @@ d3-dsv,1.0.8,New BSD
d3-ease,1.0.3,New BSD
d3-force,1.1.0,New BSD
d3-format,1.2.1,New BSD
+d3-format,1.2.2,New BSD
d3-geo,1.9.1,New BSD
d3-hierarchy,1.1.5,New BSD
d3-interpolate,1.1.6,New BSD
@@ -289,6 +289,7 @@ d3-random,1.1.0,New BSD
d3-request,1.0.6,New BSD
d3-scale,1.0.7,New BSD
d3-selection,1.2.0,New BSD
+d3-selection,1.3.0,New BSD
d3-shape,1.2.0,New BSD
d3-time,1.0.8,New BSD
d3-time-format,2.1.1,New BSD
@@ -296,8 +297,8 @@ d3-timer,1.0.7,New BSD
d3-transition,1.1.1,New BSD
d3-voronoi,1.1.2,New BSD
d3-zoom,1.7.1,New BSD
-dagre-d3-renderer,0.4.24,MIT
-dagre-layout,0.8.0,MIT
+dagre-d3-renderer,0.5.8,MIT
+dagre-layout,0.8.8,MIT
date-now,0.1.4,MIT
dateformat,3.0.3,MIT
de-indent,1.0.2,MIT
@@ -328,7 +329,6 @@ device_detector,1.0.0,LGPL
devise,4.4.3,MIT
devise-two-factor,3.0.0,MIT
diff,3.5.0,New BSD
-diff-lcs,1.3,"MIT,Artistic-2.0,GPL-2.0+"
diffie-hellman,5.0.2,MIT
diffy,3.1.0,MIT
document-register-element,1.3.0,MIT
@@ -370,6 +370,7 @@ es6-promise,3.0.2,MIT
escape-html,1.0.3,MIT
escape-string-regexp,1.0.5,MIT
escape_utils,1.1.1,MIT
+escaper,2.5.3,MIT
eslint-scope,4.0.0,Simplified BSD
esrecurse,4.2.1,Simplified BSD
estraverse,4.2.0,Simplified BSD
@@ -447,12 +448,8 @@ get-value,2.0.6,MIT
get_process_mem,0.2.0,MIT
gettext_i18n_rails,1.8.0,MIT
gettext_i18n_rails_js,1.3.0,MIT
-gitaly-proto,0.118.1,MIT
-github-linguist,5.3.3,MIT
+gitaly-proto,0.123.0,MIT
github-markup,1.7.0,MIT
-gitlab-flowdock-git-hook,1.0.1,MIT
-gitlab-gollum-lib,4.2.7.5,MIT
-gitlab-grit,2.8.2,MIT
gitlab-markup,1.6.4,MIT
gitlab-sidekiq-fetcher,0.3.0,LGPL
gitlab_omniauth-ldap,2.0.4,MIT
@@ -461,13 +458,12 @@ glob-parent,3.1.0,ISC
global-modules-path,2.1.0,Apache 2.0
globalid,0.4.1,MIT
globals,11.7.0,MIT
-gollum-grit_adapter,1.0.1,MIT
gon,6.2.0,MIT
good-listener,1.2.2,MIT
google-api-client,0.23.4,Apache 2.0
-google-protobuf,3.5.1,New BSD
-googleapis-common-protos-types,1.0.1,Apache 2.0
-googleauth,0.6.2,Apache 2.0
+google-protobuf,3.6.1,New BSD
+googleapis-common-protos-types,1.0.2,Apache 2.0
+googleauth,0.6.6,Apache 2.0
got,8.3.0,MIT
gpgme,2.0.13,LGPL-2.1+
graceful-fs,4.1.11,ISC
@@ -476,9 +472,9 @@ grape-entity,0.7.1,MIT
grape-path-helpers,1.0.6,MIT
grape_logging,1.7.0,MIT
graphiql-rails,1.4.10,MIT
-graphlib,2.1.1,MIT
+graphlibrary,2.2.0,MIT
graphql,1.8.1,MIT
-grpc,1.11.0,Apache 2.0
+grpc,1.15.0,Apache 2.0
gzip-size,5.0.0,MIT
hamlit,2.8.8,MIT
hangouts-chat,0.0.5,MIT
@@ -572,6 +568,7 @@ is-plain-obj,1.1.0,MIT
is-plain-object,2.0.4,MIT
is-promise,2.1.0,MIT
is-regex,1.0.4,MIT
+is-regexp,1.0.0,MIT
is-retry-allowed,1.1.0,MIT
is-stream,1.1.0,MIT
is-symbol,1.0.2,MIT
@@ -620,7 +617,6 @@ lazy-cache,2.0.2,MIT
lcid,2.0.0,MIT
licensee,8.9.2,MIT
lie,3.1.1,MIT
-little-plugger,1.1.4,MIT
loader-runner,2.3.0,MIT
loader-utils,1.1.0,MIT
locale,2.1.2,"ruby,LGPLv3+"
@@ -635,7 +631,6 @@ lodash.get,4.4.2,MIT
lodash.isequal,4.5.0,MIT
lodash.mergewith,4.6.0,MIT
lodash.startcase,4.4.0,MIT
-logging,2.2.2,MIT
lograge,0.10.0,MIT
loofah,2.2.2,MIT
loose-envify,1.4.0,MIT
@@ -658,6 +653,7 @@ memoist,0.16.0,MIT
memory-fs,0.4.1,MIT
merge-descriptors,1.0.1,MIT
merge-source-map,1.1.0,MIT
+mermaid,8.0.0-rc.8,MIT
method_source,0.9.0,MIT
methods,1.1.2,MIT
micromatch,3.1.10,MIT
@@ -685,11 +681,10 @@ mississippi,2.0.0,Simplified BSD
mississippi,3.0.0,Simplified BSD
mixin-deep,1.3.1,MIT
mkdirp,0.5.1,MIT
-moment,2.19.2,MIT
+moment,2.22.2,MIT
monaco-editor,0.14.3,MIT
monaco-editor-webpack-plugin,1.5.4,MIT
mousetrap,1.4.6,Apache 2.0
-mousetrap-rails,1.4.6,"MIT,Apache"
move-concurrently,1.0.1,ISC
ms,2.0.0,MIT
ms,2.1.1,MIT
@@ -760,7 +755,7 @@ opener,1.5.1,(WTFPL OR MIT)
opn,4.0.2,MIT
org-ruby,0.9.12,MIT
orm_adapter,0.5.0,MIT
-os,0.9.6,MIT
+os,1.0.0,MIT
os-browserify,0.3.0,MIT
os-homedir,1.0.2,MIT
os-locale,3.0.1,MIT
@@ -806,7 +801,6 @@ pkg-dir,3.0.0,MIT
po_to_json,1.0.1,MIT
popper.js,1.14.3,MIT
posix-character-classes,0.1.1,MIT
-posix-spawn,0.3.13,MIT
postcss,6.0.23,MIT
postcss-modules-extract-imports,1.2.0,ISC
postcss-modules-local-by-default,1.2.0,MIT
@@ -830,6 +824,8 @@ prr,1.0.1,MIT
pseudomap,1.0.2,ISC
public-encrypt,4.0.0,MIT
public_suffix,3.0.3,MIT
+puma,3.12.0,New BSD
+puma_worker_killer,0.1.0,MIT
pump,2.0.1,MIT
pump,3.0.0,MIT
pumpify,1.4.0,MIT
@@ -929,7 +925,7 @@ ruby_parser,3.9.0,MIT
rubyntlm,0.6.2,MIT
rubypants,0.2.0,BSD
rufus-scheduler,3.4.0,MIT
-rugged,0.27.4,MIT
+rugged,0.27.5,MIT
run-async,2.3.0,MIT
run-queue,1.0.3,ISC
rw,1.3.3,New BSD
@@ -949,6 +945,7 @@ sawyer,0.8.1,MIT
sax,1.2.4,ISC
schema-utils,0.4.5,MIT
schema-utils,1.0.0,MIT
+scope-css,1.2.1,MIT
seed-fu,2.3.7,MIT
select,1.1.2,MIT
select2,3.5.2-browserify,Apache*
@@ -975,8 +972,9 @@ shebang-regex,1.0.0,MIT
sidekiq,5.2.1,LGPL
sidekiq-cron,0.6.0,MIT
signal-exit,3.0.2,ISC
-signet,0.8.1,Apache 2.0
+signet,0.11.0,Apache 2.0
slack-notifier,1.5.1,MIT
+slugify,1.3.1,MIT
smooshpack,0.0.48,LGPL
snapdragon,0.8.1,MIT
snapdragon-node,2.1.1,MIT
@@ -1012,9 +1010,9 @@ string-width,1.0.2,MIT
string-width,2.1.1,MIT
string_decoder,0.10.31,MIT
string_decoder,1.1.1,MIT
-stringex,2.8.4,MIT
strip-ansi,3.0.1,MIT
strip-ansi,4.0.0,MIT
+strip-css-comments,3.0.0,MIT
strip-eof,1.0.0,MIT
strip-json-comments,2.0.1,MIT
style-loader,0.23.0,MIT
diff --git a/yarn.lock b/yarn.lock
index 79f1b757252..38e0f9d6201 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -621,15 +621,10 @@
resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.32.0.tgz#a65ab7724fa7d55be8e5cc9b2dbe3f0757432fd3"
integrity sha512-L3o8dFUd2nSkVZBwh2hCJWzNzADJ3dTBZxamND8NLosZK9/ohNhccmsQOZGyMCUHaOzm4vifaaXkAXh04UtMKA==
-"@gitlab-org/gitlab-svgs@^1.33.0":
- version "1.33.0"
- resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.33.0.tgz#068566e8ee00795f6f09f58236f08e1716f9f04a"
- integrity sha512-8ajtUHk6gQ1xosL/CO5IzHSFM/t18hx5pfzQ3cd0VuQXcyR6QKGuXTLwbYdmJDYOw1Etoo5DqDWxPEClHyZpiA==
-
-"@gitlab-org/gitlab-ui@^1.9.0":
- version "1.9.0"
- resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-ui/-/gitlab-ui-1.9.0.tgz#c47851587316f60926e8304747d1fcdd1222c779"
- integrity sha512-OQ/mhWnbeG4pmjnCGwLsyvmHDYdLh2IRnt4Jx6G9jf96oyjEHzY1rveImfqcQ2bvx9azfuI6CU9dmDSY3aWvvQ==
+"@gitlab-org/gitlab-ui@^1.10.0":
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-ui/-/gitlab-ui-1.10.0.tgz#3ac54ecaa25ea558324f0b382c97fcf9e3c4f0a5"
+ integrity sha512-kfoCKA+AmWZ3hf1wOS8W9mPJs/7lF+a01PK//+sw2MOLv6PlduJJmdN8drFuJ65o6cTJ1f9FMVB80R6D71XVKQ==
dependencies:
"@gitlab-org/gitlab-svgs" "^1.23.0"
bootstrap-vue "^2.0.0-rc.11"
@@ -648,6 +643,11 @@
eslint-plugin-promise "^4.0.1"
eslint-plugin-vue "^5.0.0-beta.3"
+"@gitlab/svgs@^1.35.0":
+ version "1.35.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.35.0.tgz#01b6a0948bb3897fbbac9f50ce23c559c514ea0e"
+ integrity sha512-XKrTniSYKG5U8+8ZqDJqoW8ORahuPBfHrfsC1dHBPvo1xA/QGJxlpUdeqSFw2O19h481ut4yW1dF+OFpIa/mrw==
+
"@sindresorhus/is@^0.7.0":
version "0.7.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"