summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitattributes1
-rw-r--r--.gitignore1
-rw-r--r--.gitlab-ci.yml258
-rw-r--r--.gitlab/CODEOWNERS.disabled (renamed from .gitlab/CODEOWNERS)0
-rw-r--r--.gitlab/issue_templates/Test plan.md4
-rw-r--r--.nvmrc2
-rw-r--r--.rubocop.yml4
-rw-r--r--.ruby-version2
-rw-r--r--CHANGELOG.md103
-rw-r--r--Dockerfile.assets4
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_PAGES_VERSION2
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile50
-rw-r--r--Gemfile.lock229
-rw-r--r--Gemfile.rails4 (renamed from Gemfile.rails5)4
-rw-r--r--Gemfile.rails4.lock (renamed from Gemfile.rails5.lock)227
-rw-r--r--PROCESS.md6
-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.js28
-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.vue7
-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/lists_dropdown.vue4
-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/ci_variable_list/ajax_variable_list.js2
-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/diff_notes/components/jump_to_discussion.js2
-rw-r--r--app/assets/javascripts/diffs/components/app.vue30
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue32
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue20
-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.vue40
-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.vue14
-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.js46
-rw-r--r--app/assets/javascripts/diffs/store/getters.js21
-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.js182
-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/due_date_select.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.vue10
-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_emoji.js11
-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.js21
-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.js89
-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.vue10
-rw-r--r--app/assets/javascripts/ide/components/ide_tree_list.vue6
-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.vue4
-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/ide/stores/modules/pipelines/actions.js2
-rw-r--r--app/assets/javascripts/init_legacy_filters.js14
-rw-r--r--app/assets/javascripts/issuable_form.js34
-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.vue338
-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.vue42
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_detail_row.vue9
-rw-r--r--app/assets/javascripts/jobs/components/stages_dropdown.vue1
-rw-r--r--app/assets/javascripts/jobs/components/stuck_block.vue24
-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.js18
-rw-r--r--app/assets/javascripts/jobs/store/mutations.js2
-rw-r--r--app/assets/javascripts/jobs/store/state.js4
-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/datefix.js28
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js128
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js2
-rw-r--r--app/assets/javascripts/lib/utils/pretty_time.js63
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js28
-rw-r--r--app/assets/javascripts/member_expiration_date.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/monitoring/components/dashboard.vue8
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue5
-rw-r--r--app/assets/javascripts/notebook/cells/output/index.vue2
-rw-r--r--app/assets/javascripts/notes.js6
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue21
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue67
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue30
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter.vue30
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue71
-rw-r--r--app/assets/javascripts/notes/components/note_awards_list.vue54
-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.js7
-rw-r--r--app/assets/javascripts/notes/stores/getters.js6
-rw-r--r--app/assets/javascripts/notes/stores/index.js3
-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/notes/stores/utils.js12
-rw-r--r--app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js2
-rw-r--r--app/assets/javascripts/pages/admin/admin.js15
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/account_and_limits.js8
-rw-r--r--app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js36
-rw-r--r--app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue65
-rw-r--r--app/assets/javascripts/pages/admin/projects/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue139
-rw-r--r--app/assets/javascripts/pages/admin/projects/index/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue187
-rw-r--r--app/assets/javascripts/pages/admin/users/index.js6
-rw-r--r--app/assets/javascripts/pages/admin/users/new/index.js6
-rw-r--r--app/assets/javascripts/pages/dashboard/issues/index.js10
-rw-r--r--app/assets/javascripts/pages/dashboard/merge_requests/index.js12
-rw-r--r--app/assets/javascripts/pages/dashboard/todos/index/todos.js15
-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/groups/settings/ci_cd/show/index.js2
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue169
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue99
-rw-r--r--app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js16
-rw-r--r--app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js16
-rw-r--r--app/assets/javascripts/pages/profiles/index.js7
-rw-r--r--app/assets/javascripts/pages/profiles/two_factor_auths/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/branches/new/index.js11
-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/graphs/charts/index.js22
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js91
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js221
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js64
-rw-r--r--app/assets/javascripts/pages/projects/init_blob.js3
-rw-r--r--app/assets/javascripts/pages/projects/init_form.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/show.js2
-rw-r--r--app/assets/javascripts/pages/projects/jobs/index/index.js16
-rw-r--r--app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue124
-rw-r--r--app/assets/javascripts/pages/projects/labels/index/index.js16
-rw-r--r--app/assets/javascripts/pages/projects/network/network.js10
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js22
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue102
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue46
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js3
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js4
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/charts/index.js59
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/index/index.js66
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/init_pipelines.js7
-rw-r--r--app/assets/javascripts/pages/projects/project.js4
-rw-r--r--app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue114
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue36
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue348
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/constants.js6
-rw-r--r--app/assets/javascripts/pages/projects/shared/project_avatar.js5
-rw-r--r--app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue11
-rw-r--r--app/assets/javascripts/pages/projects/wikis/index.js3
-rw-r--r--app/assets/javascripts/pages/search/show/search.js25
-rw-r--r--app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js2
-rw-r--r--app/assets/javascripts/pages/sessions/new/username_validator.js10
-rw-r--r--app/assets/javascripts/pages/users/index.js6
-rw-r--r--app/assets/javascripts/pages/users/user_overview_block.js5
-rw-r--r--app/assets/javascripts/pages/users/user_tabs.js110
-rw-r--r--app/assets/javascripts/performance_bar/components/simple_metric.vue46
-rw-r--r--app/assets/javascripts/performance_bar/index.js3
-rw-r--r--app/assets/javascripts/performance_bar/services/performance_bar_service.js11
-rw-r--r--app/assets/javascripts/performance_bar/stores/performance_bar_store.js4
-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.vue35
-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/profile/account/components/delete_account_modal.vue126
-rw-r--r--app/assets/javascripts/profile/gl_crop.js47
-rw-r--r--app/assets/javascripts/profile/profile.js6
-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/protected_branches/protected_branch_create.js8
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit.js59
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_create.js4
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit.js37
-rw-r--r--app/assets/javascripts/registry/components/app.vue67
-rw-r--r--app/assets/javascripts/registry/components/collapsible_container.vue101
-rw-r--r--app/assets/javascripts/registry/components/table_registry.vue102
-rw-r--r--app/assets/javascripts/registry/index.js39
-rw-r--r--app/assets/javascripts/registry/stores/mutations.js1
-rw-r--r--app/assets/javascripts/reports/components/grouped_test_reports_app.vue127
-rw-r--r--app/assets/javascripts/reports/components/issue_status_icon.vue6
-rw-r--r--app/assets/javascripts/reports/components/issues_list.vue89
-rw-r--r--app/assets/javascripts/reports/components/modal.vue40
-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/reports/components/test_issue_body.vue42
-rw-r--r--app/assets/javascripts/reports/store/actions.js8
-rw-r--r--app/assets/javascripts/reports/store/index.js13
-rw-r--r--app/assets/javascripts/reports/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/reports/store/mutations.js3
-rw-r--r--app/assets/javascripts/reports/store/state.js1
-rw-r--r--app/assets/javascripts/right_sidebar.js57
-rw-r--r--app/assets/javascripts/search_autocomplete.js15
-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/assignees/sidebar_assignees.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue20
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue134
-rw-r--r--app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue34
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue7
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue124
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue192
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/help_state.vue14
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/todo.vue8
-rw-r--r--app/assets/javascripts/sidebar/lib/sidebar_move_issue.js20
-rw-r--r--app/assets/javascripts/sidebar/mount_milestone_sidebar.js19
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js39
-rw-r--r--app/assets/javascripts/sidebar/services/sidebar_service.js14
-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.vue94
-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/dependencies.js47
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/index.js5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue167
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js5
-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/download_viewer.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue83
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.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.vue53
-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.vue4
-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.vue8
-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/pikaday.vue2
-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.vue143
-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.vue16
-rw-r--r--app/assets/javascripts/vue_shared/directives/tooltip.js8
-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.scss2
-rw-r--r--app/assets/stylesheets/framework/images.scss2
-rw-r--r--app/assets/stylesheets/framework/mixins.scss95
-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/typography.scss2
-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.scss94
-rw-r--r--app/assets/stylesheets/pages/clusters.scss35
-rw-r--r--app/assets/stylesheets/pages/diff.scss54
-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.scss10
-rw-r--r--app/assets/stylesheets/pages/note_form.scss20
-rw-r--r--app/assets/stylesheets/pages/notes.scss348
-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/admin/background_jobs_controller.rb5
-rw-r--r--app/controllers/admin/impersonation_tokens_controller.rb3
-rw-r--r--app/controllers/application_controller.rb13
-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/boards_responses.rb5
-rw-r--r--app/controllers/concerns/issuable_collections.rb36
-rw-r--r--app/controllers/concerns/members_presentation.rb7
-rw-r--r--app/controllers/concerns/merge_requests_action.rb2
-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/dashboard/milestones_controller.rb7
-rw-r--r--app/controllers/dashboard_controller.rb12
-rw-r--r--app/controllers/groups/boards_controller.rb18
-rw-r--r--app/controllers/groups/clusters/applications_controller.rb18
-rw-r--r--app/controllers/groups/clusters_controller.rb25
-rw-r--r--app/controllers/groups/milestones_controller.rb2
-rw-r--r--app/controllers/groups/settings/ci_cd_controller.rb4
-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/boards_controller.rb20
-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/deploy_keys_controller.rb11
-rw-r--r--app/controllers/projects/git_http_controller.rb5
-rw-r--r--app/controllers/projects/issues_controller.rb19
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb12
-rw-r--r--app/controllers/projects/merge_requests_controller.rb32
-rw-r--r--app/controllers/projects/mirrors_controller.rb16
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb4
-rw-r--r--app/controllers/projects_controller.rb2
-rw-r--r--app/controllers/root_controller.rb4
-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/events_finder.rb2
-rw-r--r--app/finders/group_descendants_finder.rb4
-rw-r--r--app/finders/groups_finder.rb4
-rw-r--r--app/finders/issuable_finder.rb110
-rw-r--r--app/finders/issues_finder.rb10
-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_helper.rb12
-rw-r--r--app/helpers/application_settings_helper.rb4
-rw-r--r--app/helpers/auth_helper.rb17
-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/dashboard_helper.rb4
-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/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/search_helper.rb15
-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/board_group_recent_visit.rb25
-rw-r--r--app/models/board_project_recent_visit.rb25
-rw-r--r--app/models/ci/build.rb127
-rw-r--r--app/models/ci/job_artifact.rb2
-rw-r--r--app/models/ci/pipeline.rb42
-rw-r--r--app/models/ci/pipeline_enums.rb28
-rw-r--r--app/models/clusters/applications/knative.rb55
-rw-r--r--app/models/clusters/cluster.rb60
-rw-r--r--app/models/clusters/concerns/application_status.rb7
-rw-r--r--app/models/clusters/group.rb10
-rw-r--r--app/models/clusters/kubernetes_namespace.rb52
-rw-r--r--app/models/clusters/platforms/kubernetes.rb64
-rw-r--r--app/models/commit.rb27
-rw-r--r--app/models/commit_status.rb21
-rw-r--r--app/models/commit_status_enums.rb20
-rw-r--r--app/models/compare.rb11
-rw-r--r--app/models/concerns/avatarable.rb2
-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/deployment_platform.rb1
-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.rb5
-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/redactable.rb33
-rw-r--r--app/models/concerns/storage/legacy_namespace.rb2
-rw-r--r--app/models/concerns/token_authenticatable.rb60
-rw-r--r--app/models/concerns/token_authenticatable_strategies/base.rb73
-rw-r--r--app/models/concerns/token_authenticatable_strategies/digest.rb50
-rw-r--r--app/models/concerns/token_authenticatable_strategies/insecure.rb23
-rw-r--r--app/models/concerns/with_uploads.rb2
-rw-r--r--app/models/deploy_token.rb5
-rw-r--r--app/models/deployment.rb80
-rw-r--r--app/models/diff_note.rb51
-rw-r--r--app/models/discussion_note.rb6
-rw-r--r--app/models/environment.rb7
-rw-r--r--app/models/environment_status.rb53
-rw-r--r--app/models/global_milestone.rb44
-rw-r--r--app/models/group.rb5
-rw-r--r--app/models/hooks/service_hook.rb4
-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/lfs_object.rb4
-rw-r--r--app/models/member.rb24
-rw-r--r--app/models/members_preloader.rb16
-rw-r--r--app/models/merge_request.rb28
-rw-r--r--app/models/merge_request_diff.rb13
-rw-r--r--app/models/milestone.rb18
-rw-r--r--app/models/namespace.rb8
-rw-r--r--app/models/note.rb5
-rw-r--r--app/models/personal_access_token.rb16
-rw-r--r--app/models/pool_repository.rb22
-rw-r--r--app/models/postgresql/replication_slot.rb11
-rw-r--r--app/models/project.rb34
-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/chat_message/push_message.rb47
-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.rb17
-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.rb28
-rw-r--r--app/models/service.rb1
-rw-r--r--app/models/shard.rb25
-rw-r--r--app/models/snippet.rb80
-rw-r--r--app/models/ssh_host_key.rb130
-rw-r--r--app/models/upload.rb18
-rw-r--r--app/models/user.rb87
-rw-r--r--app/models/user_callout.rb8
-rw-r--r--app/models/user_callout_enums.rb16
-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/ci/pipeline_policy.rb4
-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/ci/pipeline_presenter.rb10
-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.rb6
-rw-r--r--app/presenters/group_clusterable_presenter.rb36
-rw-r--r--app/presenters/merge_request_presenter.rb12
-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/build_details_entity.rb1
-rw-r--r--app/serializers/deployment_entity.rb1
-rw-r--r--app/serializers/environment_status_entity.rb9
-rw-r--r--app/serializers/job_entity.rb18
-rw-r--r--app/serializers/merge_request_widget_entity.rb1
-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/boards/visits/create_service.rb17
-rw-r--r--app/services/boards/visits/latest_service.rb17
-rw-r--r--app/services/ci/destroy_pipeline_service.rb13
-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.rb30
-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/delete_merged_branches_service.rb4
-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.rb27
-rw-r--r--app/services/keys/destroy_service.rb2
-rw-r--r--app/services/labels/transfer_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/create_from_issue_service.rb6
-rw-r--r--app/services/merge_requests/merge_service.rb5
-rw-r--r--app/services/merge_requests/refresh_service.rb30
-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/disable_deploy_key_service.rb13
-rw-r--r--app/services/projects/enable_deploy_key_service.rb14
-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.rb24
-rw-r--r--app/services/resource_events/merge_into_notes_service.rb2
-rw-r--r--app/services/search/group_service.rb2
-rw-r--r--app/services/submodules/update_service.rb38
-rw-r--r--app/services/system_note_service.rb8
-rw-r--r--app/services/todos/destroy/entity_leave_service.rb2
-rw-r--r--app/services/todos/destroy/private_features_service.rb6
-rw-r--r--app/services/update_deployment_service.rb53
-rw-r--r--app/services/users/build_service.rb10
-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.haml35
-rw-r--r--app/views/admin/impersonation_tokens/index.html.haml5
-rw-r--r--app/views/ci/variables/_index.html.haml4
-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/issues.html.haml4
-rw-r--r--app/views/dashboard/merge_requests.html.haml4
-rw-r--r--app/views/dashboard/snippets/index.html.haml5
-rw-r--r--app/views/devise/shared/_signin_box.html.haml6
-rw-r--r--app/views/devise/shared/_tabs_ldap.html.haml7
-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/groups/settings/ci_cd/show.html.haml2
-rw-r--r--app/views/import/bitbucket_server/status.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml4
-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/personal_access_tokens/index.html.haml12
-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/environments/_external_url.html.haml2
-rw-r--r--app/views/projects/environments/_form.html.haml15
-rw-r--r--app/views/projects/environments/_metrics_button.html.haml4
-rw-r--r--app/views/projects/environments/edit.html.haml4
-rw-r--r--app/views/projects/environments/folder.html.haml2
-rw-r--r--app/views/projects/environments/index.html.haml4
-rw-r--r--app/views/projects/environments/metrics.html.haml2
-rw-r--r--app/views/projects/environments/new.html.haml6
-rw-r--r--app/views/projects/environments/show.html.haml22
-rw-r--r--app/views/projects/environments/terminal.html.haml4
-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.haml51
-rw-r--r--app/views/projects/pipelines/show.html.haml10
-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_commit_column.html.haml2
-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/_labels_row.html.haml5
-rw-r--r--app/views/shared/_personal_access_tokens_created_container.html.haml14
-rw-r--r--app/views/shared/_personal_access_tokens_table.html.haml6
-rw-r--r--app/views/shared/boards/_show.html.haml2
-rw-r--r--app/views/shared/boards/components/_board.html.haml15
-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/_filter.html.haml32
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml23
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-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/notifications/_button.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/u2f/_authenticate.html.haml10
-rw-r--r--app/views/u2f/_register.html.haml22
-rw-r--r--app/views/users/_overview.html.haml40
-rw-r--r--app/views/users/show.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/post_receive.rb13
-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
-rwxr-xr-xbin/web5
-rwxr-xr-xbin/web_puma63
-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/27231-add-license-data-to-projects-endpoint.yml5
-rw-r--r--changelogs/unreleased/28249-add-pagination.yml5
-rw-r--r--changelogs/unreleased/28682-can-merge-branch-before-build-is-started.yml5
-rw-r--r--changelogs/unreleased/34758-create-group-clusters.yml5
-rw-r--r--changelogs/unreleased/34758-group-cluster-controller.yml5
-rw-r--r--changelogs/unreleased/40085-add-a-create_merge_request-quick-action.yml5
-rw-r--r--changelogs/unreleased/40372-prometheus-dashboard-broken-on-firefox.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/41875-allow-pipelines-to-be-deleted-by-project-owners.yml5
-rw-r--r--changelogs/unreleased/42790-improve-feedback-for-internal-git-access-checks-timeouts.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/45068-no-longer-require-a-deploy-to-start-prometheus-monitoring.yml5
-rw-r--r--changelogs/unreleased/45669-table-in-jobs-on-pipeline.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/49479-hide-unmerged-env-perf-stats.yml5
-rw-r--r--changelogs/unreleased/49726-upgrade-helm-to-2-11.yml5
-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/51306-fix-inaccessible-dropdown-for-codeless-projects.yml5
-rw-r--r--changelogs/unreleased/51335-fail-early-when-user-cannot-be-identified.yml5
-rw-r--r--changelogs/unreleased/51527-xss-in-mr-source-branch.yml5
-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/51959-branch-and-tag-name-links.yml5
-rw-r--r--changelogs/unreleased/52115-Link-button-in-markdown-editor-should-recognize-URLs.yml5
-rw-r--r--changelogs/unreleased/52122-fix-broken-whitespace-button.yml5
-rw-r--r--changelogs/unreleased/52202-consider-moving-isjobstuck-verification-to-backend.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/52383-ui-filter-assignee-none-any.yml5
-rw-r--r--changelogs/unreleased/52384-api-filter-assignee-none-any.yml5
-rw-r--r--changelogs/unreleased/52385-search-bar-for-dashboard-list.yml5
-rw-r--r--changelogs/unreleased/52545-guest-create-issue-in-group-board.yml5
-rw-r--r--changelogs/unreleased/52548-links-in-tabs-of-the-labels-index-pages-ends-with-html.yml5
-rw-r--r--changelogs/unreleased/52712-further-ui-improvements-to-profile-overview-tab.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/52780-stale-pipeline-status-cache-for-_project-after-disabling-pipelines.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/52993-ldap-rename_provider-rake-task-broken.yml5
-rw-r--r--changelogs/unreleased/53052-mg-fix-broken-ie11.yml5
-rw-r--r--changelogs/unreleased/53055-combine-date-util-functions.yml5
-rw-r--r--changelogs/unreleased/53070-fix-enable-usage-ping-link.yml5
-rw-r--r--changelogs/unreleased/53133-jobs-list.yml5
-rw-r--r--changelogs/unreleased/53155-structured-logs-params-array.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/53270-remove-mousetrap-rails.yml5
-rw-r--r--changelogs/unreleased/53273-update-moment-to-2-22-2.yml5
-rw-r--r--changelogs/unreleased/53289-update-haml_lint-to-0-28-0.yml5
-rw-r--r--changelogs/unreleased/53290-incorrect-project-list-order-select-default-label.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/53700-hashed-storagemigration.yml5
-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/54004-update-asana-to-0-8-1.yml5
-rw-r--r--changelogs/unreleased/54010-update-asciidoctor-to-1-5-8.yml5
-rw-r--r--changelogs/unreleased/54015-Markdown-Editor-improve-Cursor-placement.yml5
-rw-r--r--changelogs/unreleased/54021-empty-button.yml5
-rw-r--r--changelogs/unreleased/54093-the-default_value_for-gem-doesn-t-handle-actioncontroller-parameters-correctly.yml7
-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/ac-post-merge-pipeline.yml5
-rw-r--r--changelogs/unreleased/add-action-to-deployment.yml5
-rw-r--r--changelogs/unreleased/add-failure-reason-for-execution-timeout.yml5
-rw-r--r--changelogs/unreleased/add-scheduled-flag-to-job-entity.yml5
-rw-r--r--changelogs/unreleased/added-glob-for-ci-changes-detection.yml5
-rw-r--r--changelogs/unreleased/an-gitaly-version-0-133-0.yml5
-rw-r--r--changelogs/unreleased/an-multithreading.yml5
-rw-r--r--changelogs/unreleased/ashmckenzie-hmac-token-decode-and-tests.yml5
-rw-r--r--changelogs/unreleased/auto-devops-support-for-group-security-dashboard.yml5
-rw-r--r--changelogs/unreleased/auto_devops_kubernetes_active.yml5
-rw-r--r--changelogs/unreleased/avoid-lock-when-introduce-new-failure-reason.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-add-discord-service.yml5
-rw-r--r--changelogs/unreleased/bvl-patches-via-mail.yml5
-rw-r--r--changelogs/unreleased/bvl-preload-user-status-for-events.yml5
-rw-r--r--changelogs/unreleased/bw-automatically-navigate-to-last-board-visited.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/ce-53347_fix_impersonation_tokens.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-default-value-status-deployments.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-53298.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-not-render-emoji.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-even-even-more.yml5
-rw-r--r--changelogs/unreleased/frozen-string-lib-gitlab-even-more.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-fix-typo-in-notebook-props.yml5
-rw-r--r--changelogs/unreleased/gt-fix-typos-in-lib.yml5
-rw-r--r--changelogs/unreleased/gt-remove-unused-project-method.yml5
-rw-r--r--changelogs/unreleased/gt-rename-diffs-store-variable.yml5
-rw-r--r--changelogs/unreleased/gt-truncate-milestone-title-on-collapsed-sidebar.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/jramsay-42673-commit-tooltip.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/pl-uprade-prometheus-alertmanager.yml5
-rw-r--r--changelogs/unreleased/rails5-active-record-class-value.yml5
-rw-r--r--changelogs/unreleased/rails5-deprecated-uniq.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/ravlen-rename-secret-variables-in-codebase.yml5
-rw-r--r--changelogs/unreleased/redact-links-dev.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/rz_fix_milestone_count.yml5
-rw-r--r--changelogs/unreleased/scheduled-manual-jobs-environment-play-buttons.yml5
-rw-r--r--changelogs/unreleased/security-2717-fix-issue-title-xss.yml5
-rw-r--r--changelogs/unreleased/security-2717-xss-username-autocomplete.yml5
-rw-r--r--changelogs/unreleased/security-51113-hash_personal_access_tokens.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-gems-security.yml5
-rw-r--r--changelogs/unreleased/sh-bump-ruby-2-5-3.yml5
-rw-r--r--changelogs/unreleased/sh-fix-hipchat-ssrf.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-issue-53153.yml5
-rw-r--r--changelogs/unreleased/sh-fix-issue-53783-ce.yml5
-rw-r--r--changelogs/unreleased/sh-fix-issue-54189.yml5
-rw-r--r--changelogs/unreleased/sh-fix-search-relative-urls.yml5
-rw-r--r--changelogs/unreleased/sh-fix-wiki-security-issue-53072.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/sh-remove-local-sidekiq-admin-check.yml5
-rw-r--r--changelogs/unreleased/sh-use-nakayoshi-fork.yml5
-rw-r--r--changelogs/unreleased/sh-use-nokogiri-xml-backend.yml5
-rw-r--r--changelogs/unreleased/stateful_deployments.yml5
-rw-r--r--changelogs/unreleased/switch-rails.yml5
-rw-r--r--changelogs/unreleased/tc-index-lfs-objects-file-store.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/validate-foreign-keys-being-indexed.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/winh-pipeline-actions-dynamic-timer.yml5
-rw-r--r--changelogs/unreleased/zj-bump-gitaly-0-128.yml5
-rw-r--r--changelogs/unreleased/zj-improve-gitaly-pb.yml5
-rw-r--r--changelogs/unreleased/zj-remove-broken-storage.yml5
-rw-r--r--config/application.rb7
-rw-r--r--config/boot.rb4
-rw-r--r--config/dependency_decisions.yml9
-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/7_prometheus_metrics.rb22
-rw-r--r--config/initializers/8_metrics.rb10
-rw-r--r--config/initializers/active_record_lifecycle.rb23
-rw-r--r--config/initializers/asset_sync.rb31
-rw-r--r--config/initializers/fill_shards.rb3
-rw-r--r--config/initializers/hipchat_client_patch.rb14
-rw-r--r--config/initializers/kubeclient.rb21
-rw-r--r--config/initializers/macos.rb13
-rw-r--r--config/initializers/mysql_set_length_for_binary_indexes.rb50
-rw-r--r--config/initializers/rbtrace.rb9
-rw-r--r--config/initializers/routing_draw.rb8
-rw-r--r--config/initializers/sidekiq.rb2
-rw-r--r--config/locales/en.yml2
-rw-r--r--config/puma.example.development.rb77
-rw-r--r--config/routes.rb27
-rw-r--r--config/routes/admin.rb2
-rw-r--r--config/routes/group.rb5
-rw-r--r--config/routes/project.rb16
-rw-r--r--config/sidekiq_queues.yml1
-rw-r--r--config/unicorn.rb.example37
-rw-r--r--config/unicorn.rb.example.development67
-rw-r--r--danger/documentation/Dangerfile24
-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/20180910153412_add_token_digest_to_personal_access_tokens.rb19
-rw-r--r--db/migrate/20180910153413_add_index_to_token_digest_on_personal_access_tokens.rb17
-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/20181005110927_add_index_to_lfs_objects_file_store.rb17
-rw-r--r--db/migrate/20181005125926_add_index_to_uploads_store.rb17
-rw-r--r--db/migrate/20181010235606_create_board_project_recent_visits.rb19
-rw-r--r--db/migrate/20181014203236_create_cluster_groups.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/20181016152238_create_board_group_recent_visits.rb20
-rw-r--r--db/migrate/20181017001059_add_cluster_type_to_clusters.rb18
-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/20181030154446_add_missing_indexes_for_foreign_keys.rb65
-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/migrate/20181112103239_drop_default_value_on_status_deployments.rb14
-rw-r--r--db/post_migrate/20170717111152_cleanup_move_system_upload_folder_symlink.rb2
-rw-r--r--db/post_migrate/20180913142237_schedule_digest_personal_access_tokens.rb28
-rw-r--r--db/post_migrate/20181014121030_enqueue_redact_links.rb65
-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.rb1015
-rw-r--r--doc/README.md1
-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.md14
-rw-r--r--doc/administration/high_availability/nfs.md21
-rw-r--r--doc/administration/high_availability/redis.md12
-rw-r--r--doc/administration/high_availability/redis_source.md4
-rw-r--r--doc/administration/logs.md14
-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.md123
-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/administration/uploads.md7
-rw-r--r--doc/api/README.md3
-rw-r--r--doc/api/avatar.md2
-rw-r--r--doc/api/commits.md42
-rw-r--r--doc/api/events.md4
-rw-r--r--doc/api/issues.md105
-rw-r--r--doc/api/merge_requests.md95
-rw-r--r--doc/api/pipelines.md17
-rw-r--r--doc/api/projects.md49
-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/api/tags.md2
-rw-r--r--doc/api/users.md5
-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.md85
-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/container_registry_checkbox.pngbin4730 -> 0 bytes
-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.md14
-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/examples/test_phoenix_app_with_gitlab_ci_cd/index.md6
-rw-r--r--doc/ci/pipelines.md2
-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/variables/where_variables_can_be_used.md2
-rw-r--r--doc/ci/yaml/README.md179
-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/architecture.md189
-rw-r--r--doc/development/build_test_package.md13
-rw-r--r--doc/development/changelog.md18
-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.md180
-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/feature_flags.md2
-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/migration_style_guide.md7
-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.md99
-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.md12
-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.md4
-rw-r--r--doc/topics/autodevops/index.md23
-rw-r--r--doc/topics/autodevops/quick_start_guide.md3
-rw-r--r--doc/topics/git/troubleshooting_git.md15
-rw-r--r--doc/university/README.md13
-rw-r--r--doc/university/bookclub/booklist.md2
-rw-r--r--doc/university/glossary/README.md2
-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.md390
-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/new_project.pngbin7813 -> 0 bytes
-rw-r--r--doc/user/project/clusters/eks_and_gitlab/img/rbac.pngbin0 -> 15960 bytes
-rw-r--r--doc/user/project/clusters/eks_and_gitlab/index.md259
-rw-r--r--doc/user/project/clusters/index.md165
-rw-r--r--doc/user/project/clusters/runbooks/img/authorize-jupyter.pngbin0 -> 126425 bytes
-rw-r--r--doc/user/project/clusters/runbooks/img/demo-runbook.pngbin0 -> 132436 bytes
-rw-r--r--doc/user/project/clusters/runbooks/img/gitlab-variables.pngbin0 -> 179611 bytes
-rw-r--r--doc/user/project/clusters/runbooks/img/helm-install.pngbin0 -> 201348 bytes
-rw-r--r--doc/user/project/clusters/runbooks/img/ingress-install.pngbin0 -> 140880 bytes
-rw-r--r--doc/user/project/clusters/runbooks/img/jupyterhub-install.pngbin0 -> 116775 bytes
-rw-r--r--doc/user/project/clusters/runbooks/img/postgres-query.pngbin0 -> 209435 bytes
-rw-r--r--doc/user/project/clusters/runbooks/img/sample-runbook.pngbin0 -> 145728 bytes
-rw-r--r--doc/user/project/clusters/runbooks/index.md137
-rw-r--r--doc/user/project/clusters/serverless/img/deploy-stage.pngbin0 -> 12029 bytes
-rw-r--r--doc/user/project/clusters/serverless/img/dns-entry.pngbin0 -> 56600 bytes
-rw-r--r--doc/user/project/clusters/serverless/img/install-knative.pngbin0 -> 102861 bytes
-rw-r--r--doc/user/project/clusters/serverless/img/knative-app.pngbin0 -> 28998 bytes
-rw-r--r--doc/user/project/clusters/serverless/index.md137
-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.md31
-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/issue_board.md3
-rw-r--r--doc/user/project/issues/automatic_issue_closing.md9
-rw-r--r--doc/user/project/merge_requests/img/comment-on-any-diff-line.pngbin0 -> 177323 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_request_diff_file_navigation.pngbin111544 -> 68704 bytes
-rw-r--r--doc/user/project/merge_requests/index.md64
-rw-r--r--doc/user/project/milestones/index.md20
-rw-r--r--doc/user/project/new_ci_build_permissions_model.md2
-rw-r--r--doc/user/project/pages/getting_started_part_three.md2
-rw-r--r--doc/user/project/quick_actions.md1
-rw-r--r--doc/user/project/repository/index.md42
-rw-r--r--doc/user/project/settings/index.md2
-rw-r--r--doc/user/search/img/dashboard_links.pngbin0 -> 27164 bytes
-rw-r--r--doc/user/search/img/issues_assigned_to_you.pngbin49079 -> 50433 bytes
-rw-r--r--doc/user/search/img/issues_filter_none_any.pngbin0 -> 27717 bytes
-rw-r--r--doc/user/search/img/left_menu_bar.pngbin37433 -> 0 bytes
-rw-r--r--doc/user/search/index.md26
-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.rb61
-rw-r--r--lib/api/helpers/custom_validators.rb13
-rw-r--r--lib/api/internal.rb2
-rw-r--r--lib/api/issues.rb46
-rw-r--r--lib/api/merge_requests.rb5
-rw-r--r--lib/api/pipelines.rb15
-rw-r--r--lib/api/projects.rb9
-rw-r--r--lib/api/runner.rb3
-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.rb8
-rw-r--r--lib/api/validations/types/safe_file.rb15
-rw-r--r--lib/api/wikis.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.rb4
-rw-r--r--lib/gitlab/auth.rb4
-rw-r--r--lib/gitlab/auth/o_auth/auth_hash.rb2
-rw-r--r--lib/gitlab/auth/user_auth_finders.rb4
-rw-r--r--lib/gitlab/background_migration/digest_column.rb25
-rw-r--r--lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table.rb82
-rw-r--r--lib/gitlab/background_migration/redact_links.rb51
-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/cache/ci/project_pipeline_status.rb8
-rw-r--r--lib/gitlab/checks/change_access.rb110
-rw-r--r--lib/gitlab/checks/commit_check.rb4
-rw-r--r--lib/gitlab/checks/lfs_integrity.rb5
-rw-r--r--lib/gitlab/checks/timed_logger.rb83
-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/changes.rb2
-rw-r--r--lib/gitlab/ci/build/policy/kubernetes.rb2
-rw-r--r--lib/gitlab/ci/build/policy/refs.rb2
-rw-r--r--lib/gitlab/ci/build/policy/specification.rb2
-rw-r--r--lib/gitlab/ci/build/policy/variables.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/parsers/test/junit.rb8
-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.rb6
-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.yml79
-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.rb6
-rw-r--r--lib/gitlab/ci/yaml_processor.rb6
-rw-r--r--lib/gitlab/cluster/lifecycle_events.rb99
-rw-r--r--lib/gitlab/cluster/puma_worker_killer_initializer.rb36
-rw-r--r--lib/gitlab/conflict/file.rb38
-rw-r--r--lib/gitlab/conflict/file_collection.rb2
-rw-r--r--lib/gitlab/contributions_calendar.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/crypto_helper.rb30
-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/fogbugz_import/client.rb2
-rw-r--r--lib/gitlab/fogbugz_import/importer.rb2
-rw-r--r--lib/gitlab/fogbugz_import/project_creator.rb2
-rw-r--r--lib/gitlab/fogbugz_import/repository.rb2
-rw-r--r--lib/gitlab/gfm/reference_rewriter.rb24
-rw-r--r--lib/gitlab/gfm/uploads_rewriter.rb7
-rw-r--r--lib/gitlab/git/attributes_at_ref_parser.rb2
-rw-r--r--lib/gitlab/git/attributes_parser.rb2
-rw-r--r--lib/gitlab/git/blame.rb2
-rw-r--r--lib/gitlab/git/blob.rb5
-rw-r--r--lib/gitlab/git/branch.rb2
-rw-r--r--lib/gitlab/git/commit.rb11
-rw-r--r--lib/gitlab/git/commit_stats.rb6
-rw-r--r--lib/gitlab/git/compare.rb2
-rw-r--r--lib/gitlab/git/conflict/file.rb2
-rw-r--r--lib/gitlab/git/conflict/parser.rb2
-rw-r--r--lib/gitlab/git/conflict/resolution.rb2
-rw-r--r--lib/gitlab/git/conflict/resolver.rb8
-rw-r--r--lib/gitlab/git/diff.rb2
-rw-r--r--lib/gitlab/git/diff_collection.rb2
-rw-r--r--lib/gitlab/git/gitmodules_parser.rb2
-rw-r--r--lib/gitlab/git/hook_env.rb2
-rw-r--r--lib/gitlab/git/index.rb2
-rw-r--r--lib/gitlab/git/lfs_changes.rb6
-rw-r--r--lib/gitlab/git/lfs_pointer_file.rb2
-rw-r--r--lib/gitlab/git/operation_service.rb2
-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/path_helper.rb2
-rw-r--r--lib/gitlab/git/pre_receive_error.rb2
-rw-r--r--lib/gitlab/git/raw_diff_change.rb2
-rw-r--r--lib/gitlab/git/ref.rb2
-rw-r--r--lib/gitlab/git/remote_mirror.rb6
-rw-r--r--lib/gitlab/git/remote_repository.rb2
-rw-r--r--lib/gitlab/git/repository.rb67
-rw-r--r--lib/gitlab/git/repository_mirroring.rb2
-rw-r--r--lib/gitlab/git/tag.rb2
-rw-r--r--lib/gitlab/git/tree.rb5
-rw-r--r--lib/gitlab/git/user.rb2
-rw-r--r--lib/gitlab/git/util.rb2
-rw-r--r--lib/gitlab/git/version.rb2
-rw-r--r--lib/gitlab/git/wiki.rb20
-rw-r--r--lib/gitlab/git/wiki_file.rb2
-rw-r--r--lib/gitlab/git/wiki_page.rb2
-rw-r--r--lib/gitlab/git/wiki_page_version.rb2
-rw-r--r--lib/gitlab/git/wraps_gitaly_errors.rb17
-rw-r--r--lib/gitlab/git_access.rb22
-rw-r--r--lib/gitlab/git_post_receive.rb4
-rw-r--r--lib/gitlab/gitaly_client.rb127
-rw-r--r--lib/gitlab/gitaly_client/attributes_bag.rb2
-rw-r--r--lib/gitlab/gitaly_client/blob_service.rb23
-rw-r--r--lib/gitlab/gitaly_client/blobs_stitcher.rb2
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb16
-rw-r--r--lib/gitlab/gitaly_client/conflict_files_stitcher.rb4
-rw-r--r--lib/gitlab/gitaly_client/conflicts_service.rb2
-rw-r--r--lib/gitlab/gitaly_client/diff.rb2
-rw-r--r--lib/gitlab/gitaly_client/diff_stitcher.rb4
-rw-r--r--lib/gitlab/gitaly_client/health_check_service.rb2
-rw-r--r--lib/gitlab/gitaly_client/namespace_service.rb2
-rw-r--r--lib/gitlab/gitaly_client/notification_service.rb2
-rw-r--r--lib/gitlab/gitaly_client/operation_service.rb51
-rw-r--r--lib/gitlab/gitaly_client/queue_enumerator.rb2
-rw-r--r--lib/gitlab/gitaly_client/ref_service.rb4
-rw-r--r--lib/gitlab/gitaly_client/remote_service.rb2
-rw-r--r--lib/gitlab/gitaly_client/repository_service.rb8
-rw-r--r--lib/gitlab/gitaly_client/server_service.rb2
-rw-r--r--lib/gitlab/gitaly_client/storage_service.rb2
-rw-r--r--lib/gitlab/gitaly_client/storage_settings.rb2
-rw-r--r--lib/gitlab/gitaly_client/util.rb2
-rw-r--r--lib/gitlab/gitaly_client/wiki_file.rb2
-rw-r--r--lib/gitlab/gitaly_client/wiki_page.rb2
-rw-r--r--lib/gitlab/gitaly_client/wiki_service.rb6
-rw-r--r--lib/gitlab/gitlab_import/client.rb2
-rw-r--r--lib/gitlab/gitlab_import/importer.rb12
-rw-r--r--lib/gitlab/gitlab_import/project_creator.rb2
-rw-r--r--lib/gitlab/gon_helper.rb5
-rw-r--r--lib/gitlab/google_code_import/client.rb2
-rw-r--r--lib/gitlab/google_code_import/importer.rb2
-rw-r--r--lib/gitlab/google_code_import/project_creator.rb2
-rw-r--r--lib/gitlab/google_code_import/repository.rb2
-rw-r--r--lib/gitlab/gpg/commit.rb2
-rw-r--r--lib/gitlab/gpg/invalid_gpg_signature_updater.rb2
-rw-r--r--lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb12
-rw-r--r--lib/gitlab/grape_logging/loggers/queue_duration_logger.rb2
-rw-r--r--lib/gitlab/grape_logging/loggers/user_logger.rb2
-rw-r--r--lib/gitlab/graphql/authorize.rb2
-rw-r--r--lib/gitlab/graphql/authorize/authorize_resource.rb2
-rw-r--r--lib/gitlab/graphql/authorize/instrumentation.rb2
-rw-r--r--lib/gitlab/graphql/connections.rb2
-rw-r--r--lib/gitlab/graphql/connections/keyset_connection.rb2
-rw-r--r--lib/gitlab/graphql/errors.rb2
-rw-r--r--lib/gitlab/graphql/expose_permissions.rb2
-rw-r--r--lib/gitlab/graphql/present.rb2
-rw-r--r--lib/gitlab/graphql/present/instrumentation.rb2
-rw-r--r--lib/gitlab/graphql/variables.rb2
-rw-r--r--lib/gitlab/graphs/commits.rb2
-rw-r--r--lib/gitlab/hashed_storage/migrator.rb2
-rw-r--r--lib/gitlab/hashed_storage/rake_helper.rb2
-rw-r--r--lib/gitlab/health_checks/base_abstract_check.rb2
-rw-r--r--lib/gitlab/health_checks/db_check.rb2
-rw-r--r--lib/gitlab/health_checks/gitaly_check.rb2
-rw-r--r--lib/gitlab/health_checks/metric.rb5
-rw-r--r--lib/gitlab/health_checks/prometheus_text_format.rb2
-rw-r--r--lib/gitlab/health_checks/redis/cache_check.rb2
-rw-r--r--lib/gitlab/health_checks/redis/queues_check.rb2
-rw-r--r--lib/gitlab/health_checks/redis/redis_check.rb2
-rw-r--r--lib/gitlab/health_checks/redis/shared_state_check.rb2
-rw-r--r--lib/gitlab/health_checks/result.rb5
-rw-r--r--lib/gitlab/health_checks/simple_abstract_check.rb2
-rw-r--r--lib/gitlab/highlight.rb17
-rw-r--r--lib/gitlab/hook_data/base_builder.rb2
-rw-r--r--lib/gitlab/hook_data/issuable_builder.rb2
-rw-r--r--lib/gitlab/hook_data/issue_builder.rb2
-rw-r--r--lib/gitlab/hook_data/merge_request_builder.rb2
-rw-r--r--lib/gitlab/hook_data/note_builder.rb2
-rw-r--r--lib/gitlab/hook_data/wiki_page_builder.rb2
-rw-r--r--lib/gitlab/http_io.rb12
-rw-r--r--lib/gitlab/i18n/metadata_entry.rb2
-rw-r--r--lib/gitlab/i18n/po_linter.rb2
-rw-r--r--lib/gitlab/i18n/translation_entry.rb2
-rw-r--r--lib/gitlab/identifier.rb21
-rw-r--r--lib/gitlab/import/logger.rb2
-rw-r--r--lib/gitlab/import/merge_request_helpers.rb2
-rw-r--r--lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb2
-rw-r--r--lib/gitlab/import_export/after_export_strategies/download_notification_strategy.rb2
-rw-r--r--lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb2
-rw-r--r--lib/gitlab/import_export/after_export_strategy_builder.rb2
-rw-r--r--lib/gitlab/import_export/attribute_cleaner.rb2
-rw-r--r--lib/gitlab/import_export/attributes_finder.rb2
-rw-r--r--lib/gitlab/import_export/avatar_restorer.rb2
-rw-r--r--lib/gitlab/import_export/avatar_saver.rb2
-rw-r--r--lib/gitlab/import_export/command_line_util.rb2
-rw-r--r--lib/gitlab/import_export/error.rb2
-rw-r--r--lib/gitlab/import_export/file_importer.rb2
-rw-r--r--lib/gitlab/import_export/group_project_object_builder.rb2
-rw-r--r--lib/gitlab/import_export/hash_util.rb2
-rw-r--r--lib/gitlab/import_export/import_export.yml3
-rw-r--r--lib/gitlab/import_export/importer.rb2
-rw-r--r--lib/gitlab/import_export/json_hash_builder.rb2
-rw-r--r--lib/gitlab/import_export/lfs_restorer.rb2
-rw-r--r--lib/gitlab/import_export/lfs_saver.rb2
-rw-r--r--lib/gitlab/import_export/members_mapper.rb2
-rw-r--r--lib/gitlab/import_export/merge_request_parser.rb2
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb4
-rw-r--r--lib/gitlab/import_export/project_tree_saver.rb2
-rw-r--r--lib/gitlab/import_export/reader.rb2
-rw-r--r--lib/gitlab/import_export/relation_factory.rb4
-rw-r--r--lib/gitlab/import_export/repo_restorer.rb2
-rw-r--r--lib/gitlab/import_export/repo_saver.rb2
-rw-r--r--lib/gitlab/import_export/saver.rb2
-rw-r--r--lib/gitlab/import_export/shared.rb2
-rw-r--r--lib/gitlab/import_export/statistics_restorer.rb2
-rw-r--r--lib/gitlab/import_export/uploads_manager.rb2
-rw-r--r--lib/gitlab/import_export/uploads_restorer.rb2
-rw-r--r--lib/gitlab/import_export/uploads_saver.rb2
-rw-r--r--lib/gitlab/import_export/version_checker.rb2
-rw-r--r--lib/gitlab/import_export/version_saver.rb2
-rw-r--r--lib/gitlab/import_export/wiki_repo_saver.rb2
-rw-r--r--lib/gitlab/import_export/wiki_restorer.rb2
-rw-r--r--lib/gitlab/kubernetes/config_map.rb2
-rw-r--r--lib/gitlab/kubernetes/helm.rb5
-rw-r--r--lib/gitlab/kubernetes/helm/api.rb32
-rw-r--r--lib/gitlab/kubernetes/helm/base_command.rb10
-rw-r--r--lib/gitlab/kubernetes/helm/client_command.rb28
-rw-r--r--lib/gitlab/kubernetes/helm/init_command.rb4
-rw-r--r--lib/gitlab/kubernetes/helm/install_command.rb33
-rw-r--r--lib/gitlab/kubernetes/helm/pod.rb4
-rw-r--r--lib/gitlab/kubernetes/helm/upgrade_command.rb12
-rw-r--r--lib/gitlab/kubernetes/kube_client.rb64
-rw-r--r--lib/gitlab/kubernetes/namespace.rb6
-rw-r--r--lib/gitlab/kubernetes/pod.rb2
-rw-r--r--lib/gitlab/kubernetes/role_binding.rb11
-rw-r--r--lib/gitlab/legacy_github_import/base_formatter.rb2
-rw-r--r--lib/gitlab/legacy_github_import/branch_formatter.rb2
-rw-r--r--lib/gitlab/legacy_github_import/client.rb2
-rw-r--r--lib/gitlab/legacy_github_import/comment_formatter.rb2
-rw-r--r--lib/gitlab/legacy_github_import/importer.rb2
-rw-r--r--lib/gitlab/legacy_github_import/issuable_formatter.rb2
-rw-r--r--lib/gitlab/legacy_github_import/issue_formatter.rb2
-rw-r--r--lib/gitlab/legacy_github_import/label_formatter.rb2
-rw-r--r--lib/gitlab/legacy_github_import/milestone_formatter.rb2
-rw-r--r--lib/gitlab/legacy_github_import/project_creator.rb2
-rw-r--r--lib/gitlab/legacy_github_import/pull_request_formatter.rb2
-rw-r--r--lib/gitlab/legacy_github_import/release_formatter.rb2
-rw-r--r--lib/gitlab/legacy_github_import/user_formatter.rb2
-rw-r--r--lib/gitlab/legacy_github_import/wiki_formatter.rb2
-rw-r--r--lib/gitlab/manifest_import/manifest.rb4
-rw-r--r--lib/gitlab/manifest_import/project_creator.rb2
-rw-r--r--lib/gitlab/markup_helper.rb13
-rw-r--r--lib/gitlab/metrics/background_transaction.rb2
-rw-r--r--lib/gitlab/metrics/delta.rb2
-rw-r--r--lib/gitlab/metrics/influx_db.rb2
-rw-r--r--lib/gitlab/metrics/instrumentation.rb2
-rw-r--r--lib/gitlab/metrics/method_call.rb2
-rw-r--r--lib/gitlab/metrics/methods.rb2
-rw-r--r--lib/gitlab/metrics/methods/metric_options.rb2
-rw-r--r--lib/gitlab/metrics/metric.rb2
-rw-r--r--lib/gitlab/metrics/null_metric.rb2
-rw-r--r--lib/gitlab/metrics/prometheus.rb2
-rw-r--r--lib/gitlab/metrics/rack_middleware.rb2
-rw-r--r--lib/gitlab/metrics/requests_rack_middleware.rb2
-rw-r--r--lib/gitlab/metrics/samplers/base_sampler.rb2
-rw-r--r--lib/gitlab/metrics/samplers/influx_sampler.rb2
-rw-r--r--lib/gitlab/metrics/samplers/ruby_sampler.rb2
-rw-r--r--lib/gitlab/metrics/samplers/unicorn_sampler.rb2
-rw-r--r--lib/gitlab/metrics/sidekiq_metrics_exporter.rb2
-rw-r--r--lib/gitlab/metrics/sidekiq_middleware.rb2
-rw-r--r--lib/gitlab/metrics/subscribers/action_view.rb2
-rw-r--r--lib/gitlab/metrics/subscribers/active_record.rb2
-rw-r--r--lib/gitlab/metrics/subscribers/rails_cache.rb2
-rw-r--r--lib/gitlab/metrics/system.rb2
-rw-r--r--lib/gitlab/metrics/transaction.rb2
-rw-r--r--lib/gitlab/metrics/web_transaction.rb4
-rw-r--r--lib/gitlab/middleware/go.rb4
-rw-r--r--lib/gitlab/middleware/multipart.rb2
-rw-r--r--lib/gitlab/middleware/rails_queue_duration.rb2
-rw-r--r--lib/gitlab/middleware/read_only.rb2
-rw-r--r--lib/gitlab/middleware/read_only/controller.rb2
-rw-r--r--lib/gitlab/middleware/release_env.rb5
-rw-r--r--lib/gitlab/middleware/static.rb2
-rw-r--r--lib/gitlab/patch/draw_route.rb38
-rw-r--r--lib/gitlab/path_regex.rb2
-rw-r--r--lib/gitlab/private_commit_email.rb32
-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/repository_cache.rb4
-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/url_blocker.rb7
-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/gitlab/view/presenter/base.rb4
-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--lib/tasks/gitlab/ldap.rake2
-rw-r--r--lib/tasks/tokens.rake14
-rw-r--r--locale/ar_SA/gitlab.po4
-rw-r--r--locale/bg/gitlab.po4
-rw-r--r--locale/ca_ES/gitlab.po4
-rw-r--r--locale/cs_CZ/gitlab.po4
-rw-r--r--locale/da_DK/gitlab.po4
-rw-r--r--locale/de/gitlab.po4
-rw-r--r--locale/eo/gitlab.po4
-rw-r--r--locale/es/gitlab.po4
-rw-r--r--locale/et_EE/gitlab.po4
-rw-r--r--locale/fil_PH/gitlab.po4
-rw-r--r--locale/fr/gitlab.po4
-rw-r--r--locale/gitlab.pot286
-rw-r--r--locale/gl_ES/gitlab.po4
-rw-r--r--locale/he_IL/gitlab.po4
-rw-r--r--locale/id_ID/gitlab.po4
-rw-r--r--locale/it/gitlab.po4
-rw-r--r--locale/ja/gitlab.po4
-rw-r--r--locale/ko/gitlab.po4
-rw-r--r--locale/mn_MN/gitlab.po4
-rw-r--r--locale/nb_NO/gitlab.po4
-rw-r--r--locale/nl_NL/gitlab.po4
-rw-r--r--locale/pl_PL/gitlab.po4
-rw-r--r--locale/pt_BR/gitlab.po4
-rw-r--r--locale/ro_RO/gitlab.po4
-rw-r--r--locale/ru/gitlab.po4
-rw-r--r--locale/sq_AL/gitlab.po4
-rw-r--r--locale/tr_TR/gitlab.po4
-rw-r--r--locale/uk/gitlab.po4
-rw-r--r--locale/zh_CN/gitlab.po4
-rw-r--r--locale/zh_HK/gitlab.po4
-rw-r--r--locale/zh_TW/gitlab.po4
-rw-r--r--package.json4
-rw-r--r--qa/qa.rb67
-rw-r--r--qa/qa/factory/README.md474
-rw-r--r--qa/qa/factory/base.rb115
-rw-r--r--qa/qa/factory/dependency.rb28
-rw-r--r--qa/qa/factory/product.rb50
-rw-r--r--qa/qa/factory/resource/branch.rb77
-rw-r--r--qa/qa/factory/resource/deploy_key.rb43
-rw-r--r--qa/qa/factory/resource/deploy_token.rb48
-rw-r--r--qa/qa/factory/resource/file.rb36
-rw-r--r--qa/qa/factory/resource/fork.rb64
-rw-r--r--qa/qa/factory/resource/group.rb68
-rw-r--r--qa/qa/factory/resource/issue.rb31
-rw-r--r--qa/qa/factory/resource/kubernetes_cluster.rb60
-rw-r--r--qa/qa/factory/resource/label.rb39
-rw-r--r--qa/qa/factory/resource/merge_request.rb65
-rw-r--r--qa/qa/factory/resource/merge_request_from_fork.rb24
-rw-r--r--qa/qa/factory/resource/personal_access_token.rb27
-rw-r--r--qa/qa/factory/resource/project.rb79
-rw-r--r--qa/qa/factory/resource/project_imported_from_github.rb35
-rw-r--r--qa/qa/factory/resource/project_milestone.rb36
-rw-r--r--qa/qa/factory/resource/runner.rb47
-rw-r--r--qa/qa/factory/resource/sandbox.rb63
-rw-r--r--qa/qa/factory/resource/secret_variable.rb28
-rw-r--r--qa/qa/factory/resource/ssh_key.rb32
-rw-r--r--qa/qa/factory/resource/user.rb61
-rw-r--r--qa/qa/factory/resource/wiki.rb28
-rw-r--r--qa/qa/git/repository.rb34
-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/profile/personal_access_tokens.rb2
-rw-r--r--qa/qa/page/project/issue/show.rb34
-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/page/project/settings/ci_cd.rb4
-rw-r--r--qa/qa/page/project/settings/ci_variables.rb (renamed from qa/qa/page/project/settings/secret_variables.rb)6
-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.rb154
-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)17
-rw-r--r--qa/qa/resource/repository/push.rb (renamed from qa/qa/factory/repository/push.rb)8
-rw-r--r--qa/qa/resource/repository/wiki_push.rb (renamed from qa/qa/factory/repository/wiki_push.rb)16
-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)14
-rw-r--r--qa/qa/resource/ssh_key.rb26
-rw-r--r--qa/qa/resource/user.rb103
-rw-r--r--qa/qa/resource/wiki.rb30
-rw-r--r--qa/qa/runtime/api/client.rb2
-rw-r--r--qa/qa/runtime/env.rb35
-rw-r--r--qa/qa/runtime/logger.rb6
-rw-r--r--qa/qa/scenario/test/integration/ldap_no_tls.rb13
-rw-r--r--qa/qa/scenario/test/integration/ldap_tls.rb (renamed from qa/qa/scenario/test/integration/ldap.rb)6
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb4
-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.rb39
-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.rb7
-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.rb4
-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.rb32
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb8
-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.rb (renamed from qa/qa/specs/features/browser_ui/4_verify/secret_variable/add_secret_variable_spec.rb)14
-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.rb12
-rw-r--r--qa/qa/specs/runner.rb4
-rw-r--r--qa/spec/factory/base_spec.rb187
-rw-r--r--qa/spec/factory/dependency_spec.rb79
-rw-r--r--qa/spec/factory/product_spec.rb81
-rw-r--r--qa/spec/factory/resource/user_spec.rb36
-rw-r--r--qa/spec/git/repository_spec.rb30
-rw-r--r--qa/spec/page/logging_spec.rb6
-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/runtime/logger_spec.rb6
-rw-r--r--qa/spec/scenario/test/integration/ldap_spec.rb12
-rw-r--r--qa/spec/specs/runner_spec.rb14
-rw-r--r--rubocop/cop/code_reuse/active_record.rb1
-rwxr-xr-xscripts/build_assets_image27
-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/application_controller_spec.rb34
-rw-r--r--spec/controllers/boards/issues_controller_spec.rb19
-rw-r--r--spec/controllers/concerns/issuable_collections_spec.rb15
-rw-r--r--spec/controllers/concerns/send_file_upload_spec.rb31
-rw-r--r--spec/controllers/dashboard/milestones_controller_spec.rb9
-rw-r--r--spec/controllers/groups/boards_controller_spec.rb59
-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/profiles/personal_access_tokens_controller_spec.rb6
-rw-r--r--spec/controllers/projects/artifacts_controller_spec.rb33
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb4
-rw-r--r--spec/controllers/projects/boards_controller_spec.rb57
-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/deploy_keys_controller_spec.rb149
-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.rb38
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb52
-rw-r--r--spec/controllers/projects/milestones_controller_spec.rb2
-rw-r--r--spec/controllers/projects/mirrors_controller_spec.rb63
-rw-r--r--spec/controllers/registrations_controller_spec.rb2
-rw-r--r--spec/controllers/root_controller_spec.rb4
-rw-r--r--spec/controllers/sessions_controller_spec.rb2
-rw-r--r--spec/controllers/uploads_controller_spec.rb25
-rw-r--r--spec/db/schema_spec.rb96
-rw-r--r--spec/factories/board_group_recent_visit.rb9
-rw-r--r--spec/factories/board_project_recent_visit.rb9
-rw-r--r--spec/factories/ci/builds.rb30
-rw-r--r--spec/factories/clusters/applications/helm.rb5
-rw-r--r--spec/factories/clusters/clusters.rb15
-rw-r--r--spec/factories/clusters/kubernetes_namespaces.rb15
-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/fast_spec_helper.rb2
-rw-r--r--spec/features/admin/admin_groups_spec.rb28
-rw-r--r--spec/features/admin/admin_users_impersonation_tokens_spec.rb5
-rw-r--r--spec/features/admin/admin_users_spec.rb18
-rw-r--r--spec/features/atom/dashboard_issues_spec.rb12
-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/boards/modal_filter_spec.rb2
-rw-r--r--spec/features/boards/new_issue_spec.rb10
-rw-r--r--spec/features/calendar_spec.rb24
-rw-r--r--spec/features/commits_spec.rb60
-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/issuables_counter_spec.rb4
-rw-r--r--spec/features/dashboard/issues_filter_spec.rb31
-rw-r--r--spec/features/dashboard/issues_spec.rb27
-rw-r--r--spec/features/dashboard/label_filter_spec.rb15
-rw-r--r--spec/features/dashboard/merge_requests_spec.rb15
-rw-r--r--spec/features/dashboard/milestone_filter_spec.rb77
-rw-r--r--spec/features/dashboard/project_member_activity_index_spec.rb16
-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/milestone_spec.rb6
-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_assignee_spec.rb12
-rw-r--r--spec/features/issues/filtered_search/dropdown_emoji_spec.rb22
-rw-r--r--spec/features/issues/filtered_search/visual_tokens_spec.rb2
-rw-r--r--spec/features/issues/form_spec.rb2
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb38
-rw-r--r--spec/features/issues/resource_label_events_spec.rb7
-rw-r--r--spec/features/issues/user_creates_branch_and_merge_request_spec.rb8
-rw-r--r--spec/features/issues/user_sorts_issues_spec.rb2
-rw-r--r--spec/features/issues/user_uses_quick_actions_spec.rb58
-rw-r--r--spec/features/issues/user_views_issue_spec.rb6
-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.rb81
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb16
-rw-r--r--spec/features/merge_request/user_sees_pipelines_spec.rb3
-rw-r--r--spec/features/merge_requests/user_sorts_merge_requests_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/profiles/personal_access_tokens_spec.rb3
-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_files_spec.rb4
-rw-r--r--spec/features/projects/jobs_spec.rb94
-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/projects/wiki/user_updates_wiki_page_spec.rb298
-rw-r--r--spec/features/projects/wiki/user_views_wiki_page_spec.rb224
-rw-r--r--spec/features/projects_spec.rb16
-rw-r--r--spec/features/search/user_searches_for_wiki_pages_spec.rb2
-rw-r--r--spec/features/search/user_uses_header_search_field_spec.rb16
-rw-r--r--spec/features/user_sorts_things_spec.rb2
-rw-r--r--spec/features/users/login_spec.rb24
-rw-r--r--spec/features/users/overview_spec.rb8
-rw-r--r--spec/finders/environments_finder_spec.rb16
-rw-r--r--spec/finders/group_descendants_finder_spec.rb7
-rw-r--r--spec/finders/issues_finder_spec.rb62
-rw-r--r--spec/finders/notes_finder_spec.rb10
-rw-r--r--spec/finders/pending_todos_finder_spec.rb2
-rw-r--r--spec/finders/personal_access_tokens_finder_spec.rb8
-rw-r--r--spec/finders/pipelines_finder_spec.rb2
-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/entities/merge_request_widget.json1
-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/job/job_details.json3
-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/fixtures/trace/sample_trace24
-rw-r--r--spec/helpers/auth_helper_spec.rb10
-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/search_helper_spec.rb35
-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/ci_variable_list/ajax_variable_list_spec.js6
-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/compare_versions_spec.js126
-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/mock_data/merge_request_diffs.js42
-rw-r--r--spec/javascripts/diffs/store/actions_spec.js101
-rw-r--r--spec/javascripts/diffs/store/getters_spec.js26
-rw-r--r--spec/javascripts/diffs/store/mutations_spec.js171
-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/ide/stores/modules/pipelines/actions_spec.js2
-rw-r--r--spec/javascripts/issue_show/components/title_spec.js18
-rw-r--r--spec/javascripts/job_spec.js265
-rw-r--r--spec/javascripts/jobs/components/empty_state_spec.js1
-rw-r--r--spec/javascripts/jobs/components/job_app_spec.js221
-rw-r--r--spec/javascripts/jobs/components/job_container_item_spec.js26
-rw-r--r--spec/javascripts/jobs/components/sidebar_spec.js5
-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.js30
-rw-r--r--spec/javascripts/jobs/store/mutations_spec.js4
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js221
-rw-r--r--spec/javascripts/lib/utils/datefix_spec.js27
-rw-r--r--spec/javascripts/lib/utils/datetime_utility_spec.js (renamed from spec/javascripts/datetime_utility_spec.js)184
-rw-r--r--spec/javascripts/lib/utils/number_utility_spec.js8
-rw-r--r--spec/javascripts/lib/utils/text_markdown_spec.js58
-rw-r--r--spec/javascripts/lib/utils/text_utility_spec.js2
-rw-r--r--spec/javascripts/monitoring/dashboard_spec.js16
-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_actions_spec.js6
-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/pretty_time_spec.js135
-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/search_autocomplete_spec.js12
-rw-r--r--spec/javascripts/sidebar/assignees_spec.js84
-rw-r--r--spec/javascripts/vue_mr_widget/components/deployment_spec.js179
-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.js195
-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/expand_button_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/gl_countdown_spec.js77
-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.js30
-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/javascripts/vue_shared/directives/tooltip_spec.js39
-rw-r--r--spec/lib/api/helpers/custom_validators_spec.rb64
-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/extracts_path_spec.rb26
-rw-r--r--spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/digest_column_spec.rb46
-rw-r--r--spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb97
-rw-r--r--spec/lib/gitlab/background_migration/redact_links_spec.rb96
-rw-r--r--spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb15
-rw-r--r--spec/lib/gitlab/checks/change_access_spec.rb18
-rw-r--r--spec/lib/gitlab/checks/lfs_integrity_spec.rb5
-rw-r--r--spec/lib/gitlab/checks/timed_logger_spec.rb63
-rw-r--r--spec/lib/gitlab/ci/ansi2html_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/build/policy/changes_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/build/policy/refs_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/build/policy/variables_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/config/entry/artifacts_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/policy_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/reports_spec.rb4
-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/variables/collection/item_spec.rb4
-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_collection/merge_request_diff_spec.rb14
-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.rb15
-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/blob_spec.rb12
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb196
-rw-r--r--spec/lib/gitlab/git/lfs_changes_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/tag_spec.rb32
-rw-r--r--spec/lib/gitlab/git/tree_spec.rb14
-rw-r--r--spec/lib/gitlab/git/wraps_gitaly_errors_spec.rb28
-rw-r--r--spec/lib/gitlab/git_access_spec.rb10
-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/gitaly_client_spec.rb101
-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/identifier_spec.rb49
-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.json10
-rw-r--r--spec/lib/gitlab/import_export/project.light.json22
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb11
-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.rb83
-rw-r--r--spec/lib/gitlab/kubernetes/helm/pod_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/helm/upgrade_command_spec.rb20
-rw-r--r--spec/lib/gitlab/kubernetes/kube_client_spec.rb123
-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/patch/draw_route_spec.rb30
-rw-r--r--spec/lib/gitlab/private_commit_email_spec.rb51
-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/repository_cache_spec.rb22
-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/url_blocker_spec.rb22
-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/enqueue_redact_links_spec.rb42
-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/schedule_digest_personal_access_tokens_spec.rb46
-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/board_group_recent_visit_spec.rb64
-rw-r--r--spec/models/board_project_recent_visit_spec.rb64
-rw-r--r--spec/models/ci/build_spec.rb747
-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.rb36
-rw-r--r--spec/models/clusters/applications/runner_spec.rb24
-rw-r--r--spec/models/clusters/cluster_spec.rb93
-rw-r--r--spec/models/clusters/group_spec.rb8
-rw-r--r--spec/models/clusters/kubernetes_namespace_spec.rb103
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb103
-rw-r--r--spec/models/commit_spec.rb24
-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/concerns/redactable_spec.rb69
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb272
-rw-r--r--spec/models/deployment_spec.rb192
-rw-r--r--spec/models/environment_spec.rb146
-rw-r--r--spec/models/environment_status_spec.rb112
-rw-r--r--spec/models/global_milestone_spec.rb35
-rw-r--r--spec/models/group_spec.rb18
-rw-r--r--spec/models/lfs_object_spec.rb21
-rw-r--r--spec/models/merge_request_diff_spec.rb46
-rw-r--r--spec/models/merge_request_spec.rb174
-rw-r--r--spec/models/milestone_spec.rb37
-rw-r--r--spec/models/namespace_spec.rb11
-rw-r--r--spec/models/personal_access_token_spec.rb28
-rw-r--r--spec/models/postgresql/replication_slot_spec.rb20
-rw-r--r--spec/models/project_services/chat_message/push_message_spec.rb65
-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/hipchat_service_spec.rb18
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb6
-rw-r--r--spec/models/project_spec.rb100
-rw-r--r--spec/models/project_wiki_spec.rb250
-rw-r--r--spec/models/repository_spec.rb390
-rw-r--r--spec/models/shard_spec.rb50
-rw-r--r--spec/models/snippet_spec.rb211
-rw-r--r--spec/models/ssh_host_key_spec.rb164
-rw-r--r--spec/models/todo_spec.rb2
-rw-r--r--spec/models/upload_spec.rb64
-rw-r--r--spec/models/user_preference_spec.rb37
-rw-r--r--spec/models/user_spec.rb122
-rw-r--r--spec/models/wiki_page_spec.rb303
-rw-r--r--spec/policies/ci/pipeline_policy_spec.rb18
-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/merge_request_presenter_spec.rb9
-rw-r--r--spec/presenters/project_clusterable_presenter_spec.rb77
-rw-r--r--spec/presenters/project_presenter_spec.rb2
-rw-r--r--spec/rack_servers/configs/config.ru12
-rw-r--r--spec/rack_servers/configs/puma.rb32
-rw-r--r--spec/rack_servers/puma_spec.rb84
-rw-r--r--spec/rack_servers/unicorn_spec.rb (renamed from spec/unicorn/unicorn_spec.rb)33
-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.rb18
-rw-r--r--spec/requests/api/issues_spec.rb108
-rw-r--r--spec/requests/api/merge_requests_spec.rb17
-rw-r--r--spec/requests/api/pipelines_spec.rb61
-rw-r--r--spec/requests/api/projects_spec.rb38
-rw-r--r--spec/requests/api/runner_spec.rb18
-rw-r--r--spec/requests/api/submodules_spec.rb99
-rw-r--r--spec/requests/api/users_spec.rb4
-rw-r--r--spec/requests/api/wikis_spec.rb10
-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.rb46
-rw-r--r--spec/serializers/job_entity_spec.rb1
-rw-r--r--spec/serializers/merge_request_widget_entity_spec.rb34
-rw-r--r--spec/services/boards/visits/create_service_spec.rb53
-rw-r--r--spec/services/boards/visits/latest_service_spec.rb47
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb93
-rw-r--r--spec/services/ci/destroy_pipeline_service_spec.rb60
-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.rb212
-rw-r--r--spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb58
-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/create_from_issue_service_spec.rb10
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb60
-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.rb66
-rw-r--r--spec/services/resource_events/merge_into_notes_service_spec.rb9
-rw-r--r--spec/services/submodules/update_service_spec.rb212
-rw-r--r--spec/services/system_hooks_service_spec.rb2
-rw-r--r--spec/services/system_note_service_spec.rb14
-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.rb8
-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/features/variable_list_shared_examples.rb2
-rw-r--r--spec/support/gitaly.rb16
-rw-r--r--spec/support/helpers/cycle_analytics_helpers.rb8
-rw-r--r--spec/support/helpers/devise_helpers.rb9
-rw-r--r--spec/support/helpers/filter_item_select_helper.rb19
-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/helpers/user_login_helper.rb26
-rw-r--r--spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb16
-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/models/cluster_application_status_shared_examples.rb32
-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/emails_on_push_worker_spec.rb2
-rw-r--r--spec/workers/post_receive_spec.rb133
-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/jupyter/values.yaml2
-rw-r--r--vendor/licenses.csv53
-rw-r--r--vendor/prometheus/values.yaml4
-rw-r--r--yarn.lock137
2488 files changed, 43653 insertions, 19208 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/.gitignore b/.gitignore
index 82b3d08f7a8..aecaae95b8c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,6 +40,7 @@ eslint-report.html
/config/redis.queues.yml
/config/redis.shared_state.yml
/config/unicorn.rb
+/config/puma.rb
/config/secrets.yml
/config/sidekiq.yml
/config/registry.key
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index bcb0c8fbca8..0c662f42111 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/
@@ -32,6 +32,7 @@ variables:
GET_SOURCES_ATTEMPTS: "3"
KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json
FLAKY_RSPEC_SUITE_REPORT_PATH: rspec_flaky/report-suite.json
+ BUILD_ASSETS_IMAGE: "false"
before_script:
- bundle --version
@@ -75,15 +76,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 +124,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: []
@@ -148,13 +151,12 @@ stages:
stage: test
script:
- JOB_NAME=( $CI_JOB_NAME )
- - export CI_NODE_INDEX=${JOB_NAME[-2]}
- - export CI_NODE_TOTAL=${JOB_NAME[-1]}
- - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
+ - TEST_TOOL=${JOB_NAME[0]}
+ - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${TEST_TOOL}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true
- export SUITE_FLAKY_RSPEC_REPORT_PATH=${FLAKY_RSPEC_SUITE_REPORT_PATH}
- - export FLAKY_RSPEC_REPORT_PATH=rspec_flaky/all_${JOB_NAME[0]}_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- - export NEW_FLAKY_RSPEC_REPORT_PATH=rspec_flaky/new_${JOB_NAME[0]}_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
+ - export FLAKY_RSPEC_REPORT_PATH=rspec_flaky/all_${TEST_TOOL}_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
+ - export NEW_FLAKY_RSPEC_REPORT_PATH=rspec_flaky/new_${TEST_TOOL}_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export FLAKY_RSPEC_GENERATE_REPORT=true
- export CACHE_CLASSES=true
- cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
@@ -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
@@ -265,7 +269,7 @@ package-and-qa:
SCRIPT_NAME: trigger-build-docs
environment:
name: review-docs/$CI_COMMIT_REF_SLUG
- # DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are secret variables
+ # DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are CI variables
# Discussion: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14236/diffs#note_40140693
url: http://$CI_ENVIRONMENT_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
on_stop: review-docs-cleanup
@@ -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:
@@ -456,129 +461,21 @@ danger-review:
- yarn install --frozen-lockfile --cache-folder .yarn-cache
- danger --fail-on-errors=true
-rspec-pg 0 30: *rspec-metadata-pg
-rspec-pg 1 30: *rspec-metadata-pg
-rspec-pg 2 30: *rspec-metadata-pg
-rspec-pg 3 30: *rspec-metadata-pg
-rspec-pg 4 30: *rspec-metadata-pg
-rspec-pg 5 30: *rspec-metadata-pg
-rspec-pg 6 30: *rspec-metadata-pg
-rspec-pg 7 30: *rspec-metadata-pg
-rspec-pg 8 30: *rspec-metadata-pg
-rspec-pg 9 30: *rspec-metadata-pg
-rspec-pg 10 30: *rspec-metadata-pg
-rspec-pg 11 30: *rspec-metadata-pg
-rspec-pg 12 30: *rspec-metadata-pg
-rspec-pg 13 30: *rspec-metadata-pg
-rspec-pg 14 30: *rspec-metadata-pg
-rspec-pg 15 30: *rspec-metadata-pg
-rspec-pg 16 30: *rspec-metadata-pg
-rspec-pg 17 30: *rspec-metadata-pg
-rspec-pg 18 30: *rspec-metadata-pg
-rspec-pg 19 30: *rspec-metadata-pg
-rspec-pg 20 30: *rspec-metadata-pg
-rspec-pg 21 30: *rspec-metadata-pg
-rspec-pg 22 30: *rspec-metadata-pg
-rspec-pg 23 30: *rspec-metadata-pg
-rspec-pg 24 30: *rspec-metadata-pg
-rspec-pg 25 30: *rspec-metadata-pg
-rspec-pg 26 30: *rspec-metadata-pg
-rspec-pg 27 30: *rspec-metadata-pg
-rspec-pg 28 30: *rspec-metadata-pg
-rspec-pg 29 30: *rspec-metadata-pg
-
-rspec-mysql 0 30: *rspec-metadata-mysql
-rspec-mysql 1 30: *rspec-metadata-mysql
-rspec-mysql 2 30: *rspec-metadata-mysql
-rspec-mysql 3 30: *rspec-metadata-mysql
-rspec-mysql 4 30: *rspec-metadata-mysql
-rspec-mysql 5 30: *rspec-metadata-mysql
-rspec-mysql 6 30: *rspec-metadata-mysql
-rspec-mysql 7 30: *rspec-metadata-mysql
-rspec-mysql 8 30: *rspec-metadata-mysql
-rspec-mysql 9 30: *rspec-metadata-mysql
-rspec-mysql 10 30: *rspec-metadata-mysql
-rspec-mysql 11 30: *rspec-metadata-mysql
-rspec-mysql 12 30: *rspec-metadata-mysql
-rspec-mysql 13 30: *rspec-metadata-mysql
-rspec-mysql 14 30: *rspec-metadata-mysql
-rspec-mysql 15 30: *rspec-metadata-mysql
-rspec-mysql 16 30: *rspec-metadata-mysql
-rspec-mysql 17 30: *rspec-metadata-mysql
-rspec-mysql 18 30: *rspec-metadata-mysql
-rspec-mysql 19 30: *rspec-metadata-mysql
-rspec-mysql 20 30: *rspec-metadata-mysql
-rspec-mysql 21 30: *rspec-metadata-mysql
-rspec-mysql 22 30: *rspec-metadata-mysql
-rspec-mysql 23 30: *rspec-metadata-mysql
-rspec-mysql 24 30: *rspec-metadata-mysql
-rspec-mysql 25 30: *rspec-metadata-mysql
-rspec-mysql 26 30: *rspec-metadata-mysql
-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:
+ <<: *rspec-metadata-pg
+ parallel: 50
+
+rspec-mysql:
+ <<: *rspec-metadata-mysql
+ parallel: 50
+
+rspec-pg-rails4:
+ <<: *rspec-metadata-pg-rails4
+ parallel: 50
+
+rspec-mysql-rails4:
+ <<: *rspec-metadata-mysql-rails4
+ parallel: 50
static-analysis:
<<: *dedicated-no-docs-no-db-pull-cache-job
@@ -588,7 +485,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 +521,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 +592,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 +604,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 +672,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 +697,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 +720,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
@@ -830,6 +739,39 @@ qa:selectors:
- bundle install
- bundle exec bin/qa Test::Sanity::Selectors
+.qa-frontend-node: &qa-frontend-node
+ stage: test
+ variables:
+ NODE_OPTIONS: --max_old_space_size=3584
+ cache:
+ key: "$CI_JOB_NAME"
+ paths:
+ - .yarn-cache/
+ dependencies: []
+ before_script: []
+ script:
+ - date
+ - yarn install --frozen-lockfile --cache-folder .yarn-cache
+ - date
+ - yarn run webpack-prod
+
+qa-frontend-node:6:
+ <<: *qa-frontend-node
+ image: node:6-alpine
+
+qa-frontend-node:8:
+ <<: *qa-frontend-node
+ image: node:8-alpine
+
+qa-frontend-node:10:
+ <<: *qa-frontend-node
+ image: node:10-alpine
+
+qa-frontend-node:latest:
+ <<: *qa-frontend-node
+ image: node:alpine
+ allow_failure: true
+
coverage:
# Don't include dedicated-no-docs-no-db-pull-cache-job here since we need to
# download artifacts from all the rspec jobs instead of from setup-test-env only
@@ -852,9 +794,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
@@ -881,6 +821,8 @@ pages:
- mv coverage-javascript/ public/coverage-javascript/ || true
- mv eslint-report.html public/ || true
- mv webpack-report/ public/webpack-report/ || true
+ - cp .public/assets/application-*.css public/application.css || true
+ - cp .public/assets/application-*.css.gz public/application.css.gz || true
artifacts:
paths:
- public
@@ -908,6 +850,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 +861,7 @@ no_ee_check:
variables:
SETUP_DB: "false"
before_script: []
+ dependencies: []
cache: {}
script:
- scripts/no-ee-check
@@ -946,7 +890,6 @@ review:
- download_gitlab_chart
- ensure_namespace
- install_tiller
- - create_secret
- install_external_dns
- deploy
environment:
@@ -1011,6 +954,5 @@ schedule:review_apps_cleanup:
- schedules@gitlab-org/gitlab-ee
kubernetes: active
except:
- - master
- tags
- /(^docs[\/-].*|.*-docs$)/
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS.disabled
index a4b773b15a9..a4b773b15a9 100644
--- a/.gitlab/CODEOWNERS
+++ b/.gitlab/CODEOWNERS.disabled
diff --git a/.gitlab/issue_templates/Test plan.md b/.gitlab/issue_templates/Test plan.md
index db8e30c419c..a3c3f4a6509 100644
--- a/.gitlab/issue_templates/Test plan.md
+++ b/.gitlab/issue_templates/Test plan.md
@@ -64,7 +64,7 @@ Some features might be simple enough that they only involve one Component, while
more complex features could involve multiple or even all.
Example (from https://gitlab.com/gitlab-org/gitlab-ce/issues/50353):
-* Respository is
+* Repository is
* Intuitive
* It's easy to select the desired file template
* It doesn't require unnecessary actions to save the change
@@ -93,4 +93,4 @@ When adding new automated tests, please keep [testing levels](https://docs.gitla
in mind.
-->
-/label ~Quality ~"test plan" \ No newline at end of file
+/label ~Quality ~"test plan"
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/.rubocop.yml b/.rubocop.yml
index 0f4018326a1..a95ded8af1c 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -76,10 +76,14 @@ Naming/FileName:
- 'qa/qa/specs/**/*'
- 'qa/bin/*'
- 'config/**/*'
+ - 'ee/config/**/*'
- 'lib/generators/**/*'
- 'locale/unfound_translations.rb'
- 'ee/locale/unfound_translations.rb'
- 'ee/lib/generators/**/*'
+ - 'qa/qa/scenario/test/integration/ldap_no_tls.rb'
+ - 'qa/qa/scenario/test/integration/ldap_tls.rb'
+
IgnoreExecutableScripts: true
AllowedAcronyms:
- EE
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 2fc5b24aa39..54ea014a679 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,57 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 11.4.6 (2018-11-18)
+
+### Security (1 change)
+
+- Escape user fullname while rendering autocomplete template to prevent XSS.
+
+
+## 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.
+
+## 11.4.2 (2018-10-25)
+
+### Security (5 changes)
+
+- Escape entity title while autocomplete template rendering to prevent XSS. !2571
+- Persist only SHA digest of PersonalAccessToken#token.
+- Redact personal tokens in unsubscribe links.
+- Block loopback addresses in UrlBlocker.
+- Validate Wiki attachments are valid temporary files.
+
+
+## 11.4.1 (2018-10-23)
+
+### Security (2 changes)
+
+- Fix XSS in merge request source branch name.
+- Prevent SSRF attacks in HipChat integration.
+
+
## 11.4.0 (2018-10-22)
### Security (9 changes)
@@ -227,6 +278,36 @@ entry.
- Check frozen string in style builds. (gfyoung)
+## 11.3.10 (2018-11-18)
+
+### Security (1 change)
+
+- Escape user fullname while rendering autocomplete template to prevent XSS.
+
+
+## 11.3.9 (2018-10-31)
+
+### Security (1 change)
+
+- Monkey kubeclient to not follow any redirects.
+
+
+## 11.3.8 (2018-10-27)
+
+- No changes.
+
+## 11.3.7 (2018-10-26)
+
+### Security (6 changes)
+
+- Escape entity title while autocomplete template rendering to prevent XSS. !2557
+- Persist only SHA digest of PersonalAccessToken#token.
+- Fix XSS in merge request source branch name.
+- Redact personal tokens in unsubscribe links.
+- Prevent SSRF attacks in HipChat integration.
+- Validate Wiki attachments are valid temporary files.
+
+
## 11.3.6 (2018-10-17)
- No changes.
@@ -516,6 +597,28 @@ 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.
+
+## 11.2.6 (2018-10-26)
+
+### Security (5 changes)
+
+- Escape entity title while autocomplete template rendering to prevent XSS. !2558
+- Fix XSS in merge request source branch name.
+- Redact personal tokens in unsubscribe links.
+- Persist only SHA digest of PersonalAccessToken#token.
+- Prevent SSRF attacks in HipChat integration.
+
+
## 11.2.5 (2018-10-05)
### Security (3 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..244fb7efb4c 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.126.0
+0.133.0
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
index 6085e946503..f0bb29e7638 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-1.2.1
+1.3.0
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 9da0a092a0d..6da4de57dc6 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-8.4.0 \ No newline at end of file
+8.4.1
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 c442ed9065e..2c0eb0cbce4 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,13 +1,12 @@
# --- 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 = {}
-gem_versions['activerecord_sane_schema_dumper'] = rails5? ? '1.0' : '0.2'
-gem_versions['default_value_for'] = rails5? ? '~> 3.0.5' : '~> 3.0.0'
-gem_versions['rails'] = rails5? ? '5.0.7' : '4.2.10'
-gem_versions['rails-i18n'] = rails5? ? '~> 5.1' : '~> 4.0.9'
+gem_versions['activerecord_sane_schema_dumper'] = rails5? ? '1.0' : '0.2'
+gem_versions['rails'] = rails5? ? '5.0.7' : '4.2.10'
+gem_versions['rails-i18n'] = rails5? ? '~> 5.1' : '~> 4.0.9'
# --- The end of special code for migrating to Rails 5.0 ---
source 'https://rubygems.org'
@@ -15,13 +14,20 @@ source 'https://rubygems.org'
gem 'rails', gem_versions['rails']
gem 'rails-deprecated_sanitizer', '~> 1.0.3'
+# Improves copy-on-write performance for MRI
+gem 'nakayoshi_fork', '~> 0.0.4'
+
# Responders respond_to and respond_with
gem 'responders', '~> 2.0'
gem 'sprockets', '~> 3.7.0'
# Default values for AR models
-gem 'default_value_for', gem_versions['default_value_for']
+if rails5?
+ gem 'gitlab-default_value_for', '~> 3.1.1', require: 'default_value_for'
+else
+ gem 'default_value_for', '~> 3.0.0'
+end
# Supported DBs
gem 'mysql2', '~> 0.4.10', group: :mysql
@@ -124,7 +130,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'
@@ -133,7 +139,7 @@ gem 'rdoc', '~> 6.0'
gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
-gem 'asciidoctor', '~> 1.5.6'
+gem 'asciidoctor', '~> 1.5.8'
gem 'asciidoctor-plantuml', '0.0.8'
gem 'rouge', '~> 3.1'
gem 'truncato', '~> 0.7.9'
@@ -153,6 +159,11 @@ group :unicorn do
gem 'unicorn-worker-killer', '~> 0.4.4'
end
+group :puma do
+ gem 'puma', '~> 3.12', require: false
+ gem 'puma_worker_killer', require: false
+end
+
# State machine
gem 'state_machines-activerecord', '~> 0.5.1'
@@ -199,6 +210,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'
@@ -215,7 +229,7 @@ gem 'slack-notifier', '~> 1.5.1'
gem 'hangouts-chat', '~> 0.0.5'
# Asana integration
-gem 'asana', '~> 0.6.0'
+gem 'asana', '~> 0.8.1'
# FogBugz integration
gem 'ruby-fogbugz', '~> 0.2.1'
@@ -239,9 +253,6 @@ gem 'rack-attack', '~> 4.4.1'
# Ace editor
gem 'ace-rails-ap', '~> 4.1.0'
-# Keyboard shortcuts
-gem 'mousetrap-rails', '~> 1.4.6'
-
# Detect and convert string character encoding
gem 'charlock_holmes', '~> 0.7.5'
@@ -310,7 +321,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'
@@ -337,7 +348,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'
@@ -352,14 +363,14 @@ 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
gem 'benchmark-ips', '~> 2.3.0', require: false
gem 'license_finder', '~> 5.4', require: false
- gem 'knapsack', '~> 1.16'
+ gem 'knapsack', '~> 1.17'
gem 'activerecord_sane_schema_dumper', gem_versions['activerecord_sane_schema_dumper']
@@ -378,7 +389,7 @@ group :test do
gem 'rails-controller-testing' if rails5? # Rails5 only gem.
gem 'test_after_commit', '~> 1.1' unless rails5? # Remove this gem when migrated to rails 5.0. It's been integrated to rails 5.0.
gem 'sham_rack', '~> 1.3.6'
- gem 'concurrent-ruby', '~> 1.0.5'
+ gem 'concurrent-ruby', '~> 1.1'
gem 'test-prof', '~> 0.2.5'
gem 'rspec_junit_formatter'
end
@@ -414,7 +425,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'
@@ -429,6 +440,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 bf16bef4f32..380f1dbc89f 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,20 +52,15 @@ GEM
public_suffix (>= 2.0.2, < 4.0)
aes_key_wrap (1.0.1)
akismet (2.0.0)
- arel (6.0.4)
- asana (0.6.0)
+ arel (7.1.4)
+ asana (0.8.1)
faraday (~> 0.9)
faraday_middleware (~> 0.9)
faraday_middleware-multi_json (~> 0.0)
oauth2 (~> 1.0)
- asciidoctor (1.5.6.2)
+ asciidoctor (1.5.8)
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)
@@ -130,9 +128,9 @@ GEM
concord (0.1.5)
adamantium (~> 0.2.0)
equalizer (~> 0.0.9)
- concurrent-ruby (1.0.5)
- concurrent-ruby-ext (1.0.5)
- concurrent-ruby (= 1.0.5)
+ concurrent-ruby (1.1.3)
+ concurrent-ruby-ext (1.1.3)
+ concurrent-ruby (= 1.1.3)
connection_pool (2.2.2)
crack (0.4.3)
safe_yaml (~> 1.0.0)
@@ -142,14 +140,12 @@ 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)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
device_detector (1.0.0)
@@ -167,6 +163,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 +203,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 +272,12 @@ 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-default_value_for (3.1.1)
+ activerecord (>= 3.2.0, < 6.0)
+ 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.1)
concurrent-ruby (~> 1.0)
icalendar (2.4.1)
ice_nine (0.11.2)
@@ -416,7 +415,7 @@ GEM
kaminari-core (= 1.0.1)
kaminari-core (1.0.1)
kgio (2.10.0)
- knapsack (1.16.0)
+ knapsack (1.17.0)
rake
kubeclient (3.1.0)
http (~> 2.2.2)
@@ -445,7 +444,7 @@ GEM
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
- loofah (2.2.2)
+ loofah (2.2.3)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.0)
@@ -454,16 +453,15 @@ GEM
memoist (0.16.0)
memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1)
- method_source (0.9.0)
- mime-types (3.1)
+ method_source (0.9.2)
+ 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)
mini_portile2 (2.3.0)
minitest (5.7.0)
- mousetrap-rails (1.4.6)
msgpack (1.2.4)
multi_json (1.13.1)
multi_xml (0.6.0)
@@ -472,10 +470,12 @@ GEM
mustermann-grape (1.0.0)
mustermann (~> 1.0.0)
mysql2 (0.4.10)
+ nakayoshi_fork (0.0.4)
net-ldap (0.16.0)
net-ssh (5.0.1)
netrc (0.11.0)
- nokogiri (1.8.4)
+ nio4r (2.3.1)
+ nokogiri (1.8.5)
mini_portile2 (~> 2.3.0)
nokogumbo (1.5.0)
nokogiri
@@ -547,7 +547,7 @@ GEM
orm_adapter (0.5.0)
os (1.0.0)
parallel (1.12.1)
- parser (2.5.1.0)
+ parser (2.5.3.0)
ast (~> 2.4.0)
parslet (1.8.2)
peek (1.0.1)
@@ -598,8 +598,12 @@ GEM
pry-rails (0.3.6)
pry (>= 0.10.4)
public_suffix (3.0.3)
+ puma (3.12.0)
+ puma_worker_killer (0.1.0)
+ get_process_mem (~> 0.2)
+ puma (>= 2.7, < 4)
pyu-ruby-sasl (0.0.3.3)
- rack (1.6.10)
+ rack (2.0.6)
rack-accept (0.4.5)
rack (>= 0.4)
rack-attack (4.4.1)
@@ -611,37 +615,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)
@@ -679,7 +688,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)
@@ -732,8 +741,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
@@ -759,8 +767,8 @@ GEM
ruby-progressbar (1.9.0)
ruby-saml (1.7.2)
nokogiri (>= 1.5.10)
- ruby_parser (3.9.0)
- sexp_processor (~> 4.1)
+ ruby_parser (3.11.0)
+ sexp_processor (~> 4.9)
rubyntlm (0.6.2)
rubypants (0.2.0)
rubyzip (1.2.2)
@@ -800,12 +808,12 @@ GEM
sentry-raven (2.7.2)
faraday (>= 0.7.6, < 1.0)
settingslogic (2.0.9)
- sexp_processor (4.9.0)
+ sexp_processor (4.11.0)
sham_rack (1.3.6)
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)
@@ -850,8 +858,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)
@@ -914,6 +920,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
@@ -929,14 +938,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)
+ asana (~> 0.8.1)
+ asciidoctor (~> 1.5.8)
asciidoctor-plantuml (= 0.0.8)
- asset_sync (~> 2.4)
attr_encrypted (~> 3.1.0)
awesome_print
babosa (~> 1.0.2)
@@ -945,7 +953,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)
@@ -959,16 +967,16 @@ DEPENDENCIES
chronic (~> 0.10.2)
chronic_duration (~> 0.10.6)
commonmarker (~> 0.17)
- concurrent-ruby (~> 1.0.5)
+ concurrent-ruby (~> 1.1)
connection_pool (~> 2.0)
creole (~> 0.5.0)
database_cleaner (~> 1.5.0)
deckar01-task_list (= 2.0.0)
- 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)
@@ -978,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)
@@ -997,9 +1005,10 @@ 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-default_value_for (~> 3.1.1)
+ gitlab-markup (~> 1.6.5)
gitlab-sidekiq-fetcher
gitlab-styles (~> 2.4)
gitlab_omniauth-ldap (~> 2.0.4)
@@ -1014,7 +1023,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
@@ -1031,7 +1040,7 @@ DEPENDENCIES
json-schema (~> 2.8.0)
jwt (~> 1.5.6)
kaminari (~> 1.0)
- knapsack (~> 1.16)
+ knapsack (~> 1.17)
kubeclient (~> 3.1.0)
letter_opener_web (~> 1.3.0)
license_finder (~> 5.4)
@@ -1042,8 +1051,8 @@ DEPENDENCIES
method_source (~> 0.8)
mini_magick
minitest (~> 5.7.0)
- mousetrap-rails (~> 1.4.6)
mysql2 (~> 0.4.10)
+ nakayoshi_fork (~> 0.0.4)
net-ldap
net-ssh (~> 5.0)
nokogiri (~> 1.8.2)
@@ -1076,13 +1085,16 @@ DEPENDENCIES
prometheus-client-mmap (~> 0.9.4)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
+ puma (~> 3.12)
+ puma_worker_killer
rack-attack (~> 4.4.1)
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)
@@ -1135,7 +1147,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)
@@ -1154,4 +1165,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 81547303ed2..ead404c5f27 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,20 +49,15 @@ GEM
public_suffix (>= 2.0.2, < 4.0)
aes_key_wrap (1.0.1)
akismet (2.0.0)
- arel (7.1.4)
- asana (0.6.0)
+ arel (6.0.4)
+ asana (0.8.1)
faraday (~> 0.9)
faraday_middleware (~> 0.9)
faraday_middleware-multi_json (~> 0.0)
oauth2 (~> 1.0)
- asciidoctor (1.5.6.2)
+ asciidoctor (1.5.8)
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)
@@ -133,9 +125,9 @@ GEM
concord (0.1.5)
adamantium (~> 0.2.0)
equalizer (~> 0.0.9)
- concurrent-ruby (1.0.5)
- concurrent-ruby-ext (1.0.5)
- concurrent-ruby (= 1.0.5)
+ concurrent-ruby (1.1.3)
+ concurrent-ruby-ext (1.1.3)
+ concurrent-ruby (= 1.1.3)
connection_pool (2.2.2)
crack (0.4.3)
safe_yaml (~> 1.0.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)
@@ -419,7 +412,7 @@ GEM
kaminari-core (= 1.0.1)
kaminari-core (1.0.1)
kgio (2.10.0)
- knapsack (1.16.0)
+ knapsack (1.17.0)
rake
kubeclient (3.1.0)
http (~> 2.2.2)
@@ -448,7 +441,7 @@ GEM
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
- loofah (2.2.2)
+ loofah (2.2.3)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.0)
@@ -458,15 +451,14 @@ 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)
mini_portile2 (2.3.0)
minitest (5.7.0)
- mousetrap-rails (1.4.6)
msgpack (1.2.4)
multi_json (1.13.1)
multi_xml (0.6.0)
@@ -475,11 +467,11 @@ GEM
mustermann-grape (1.0.0)
mustermann (~> 1.0.0)
mysql2 (0.4.10)
+ nakayoshi_fork (0.0.4)
net-ldap (0.16.0)
net-ssh (5.0.1)
netrc (0.11.0)
- nio4r (2.3.1)
- nokogiri (1.8.4)
+ nokogiri (1.8.5)
mini_portile2 (~> 2.3.0)
nokogumbo (1.5.0)
nokogiri
@@ -551,7 +543,7 @@ GEM
orm_adapter (0.5.0)
os (1.0.0)
parallel (1.12.1)
- parser (2.5.1.0)
+ parser (2.5.3.0)
ast (~> 2.4.0)
parslet (1.8.2)
peek (1.0.1)
@@ -602,8 +594,12 @@ GEM
pry-rails (0.3.6)
pry (>= 0.10.4)
public_suffix (3.0.3)
+ puma (3.12.0)
+ puma_worker_killer (0.1.0)
+ 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)
@@ -615,42 +611,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)
@@ -688,7 +679,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)
@@ -741,7 +732,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
@@ -767,8 +759,8 @@ GEM
ruby-progressbar (1.9.0)
ruby-saml (1.7.2)
nokogiri (>= 1.5.10)
- ruby_parser (3.9.0)
- sexp_processor (~> 4.1)
+ ruby_parser (3.11.0)
+ sexp_processor (~> 4.9)
rubyntlm (0.6.2)
rubypants (0.2.0)
rubyzip (1.2.2)
@@ -808,12 +800,12 @@ GEM
sentry-raven (2.7.2)
faraday (>= 0.7.6, < 1.0)
settingslogic (2.0.9)
- sexp_processor (4.9.0)
+ sexp_processor (4.11.0)
sham_rack (1.3.6)
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)
@@ -858,6 +850,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)
@@ -920,9 +914,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
@@ -938,14 +929,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)
+ asana (~> 0.8.1)
+ asciidoctor (~> 1.5.8)
asciidoctor-plantuml (= 0.0.8)
- asset_sync (~> 2.4)
attr_encrypted (~> 3.1.0)
awesome_print
babosa (~> 1.0.2)
@@ -954,7 +944,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)
@@ -968,16 +958,17 @@ DEPENDENCIES
chronic (~> 0.10.2)
chronic_duration (~> 0.10.6)
commonmarker (~> 0.17)
- concurrent-ruby (~> 1.0.5)
+ concurrent-ruby (~> 1.1)
connection_pool (~> 2.0)
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)
@@ -987,7 +978,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)
@@ -1006,9 +997,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)
@@ -1023,7 +1014,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
@@ -1040,7 +1031,7 @@ DEPENDENCIES
json-schema (~> 2.8.0)
jwt (~> 1.5.6)
kaminari (~> 1.0)
- knapsack (~> 1.16)
+ knapsack (~> 1.17)
kubeclient (~> 3.1.0)
letter_opener_web (~> 1.3.0)
license_finder (~> 5.4)
@@ -1051,8 +1042,8 @@ DEPENDENCIES
method_source (~> 0.8)
mini_magick
minitest (~> 5.7.0)
- mousetrap-rails (~> 1.4.6)
mysql2 (~> 0.4.10)
+ nakayoshi_fork (~> 0.0.4)
net-ldap
net-ssh (~> 5.0)
nokogiri (~> 1.8.2)
@@ -1085,14 +1076,15 @@ DEPENDENCIES
prometheus-client-mmap (~> 0.9.4)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
+ puma (~> 3.12)
+ puma_worker_killer
rack-attack (~> 4.4.1)
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)
@@ -1145,6 +1137,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)
@@ -1163,4 +1156,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.16.6
+ 1.17.1
diff --git a/PROCESS.md b/PROCESS.md
index 5fc2c4cf1df..aadd6a321f0 100644
--- a/PROCESS.md
+++ b/PROCESS.md
@@ -95,6 +95,12 @@ picked into the stable branches) up to the 19th of the month. Such merge
requests should have the ~"feature flag" label assigned, and don't require a
corresponding exception request to be created.
+In order to build the final package and present the feature for self-hosted
+customers, the feature flag should be removed. This should happen before the
+22nd, ideally _at least_ 2 days before. That means MRs with feature
+flags being picked at the 19th would have a quite tight schedule, so picking
+these _earlier_ is preferable.
+
While rare, release managers may decide to reject picking a change into a stable
branch, even when feature flags are used. This might be necessary if the changes
are deemed problematic, too invasive, or there simply isn't enough time to
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..ca1662313c2 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/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..0eff922d93c 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/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..5b64ea1080c 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/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..d01465232a7 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/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 75477ebb3b3..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,
@@ -53,26 +53,28 @@ export default Vue.extend({
const { issuesSize } = this.list;
return `${n__('%d issue', '%d issues', issuesSize)}`;
},
+ 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) {
@@ -83,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;
}
@@ -104,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..a04b828e277 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/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 030288a1c9d..2a96d1443e1 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -1,6 +1,6 @@
<script>
import $ from 'jquery';
-import { Button } from '@gitlab-org/gitlab-ui';
+import { GlButton } from '@gitlab/ui';
import eventHub from '../eventhub';
import ProjectSelect from './project_select.vue';
import ListIssue from '../models/issue';
@@ -10,7 +10,7 @@ export default {
name: 'BoardNewIssue',
components: {
ProjectSelect,
- 'gl-button': Button,
+ GlButton,
},
props: {
groupId: {
@@ -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..48c46ae32f1 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/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..d6c87132843
--- /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/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..a9803c8ab5d
--- /dev/null
+++ b/app/assets/javascripts/boards/components/issue_time_estimate.vue
@@ -0,0 +1,48 @@
+<script>
+import { GlTooltip } from '@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..fc0e3e46b88 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/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/lists_dropdown.vue b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
index 3baac08d411..c502e32d169 100644
--- a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
+++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
@@ -1,12 +1,12 @@
<script>
-import { Link } from '@gitlab-org/gitlab-ui';
+import { GlLink } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import ModalStore from '../../stores/modal_store';
import boardsStore from '../../stores/boards_store';
export default {
components: {
- 'gl-link': Link,
+ GlLink,
Icon,
},
data() {
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 4e8fe16160a..83e6e237757 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/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, {}, 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/ci_variable_list/ajax_variable_list.js b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js
index 1089d0a72d3..c7a917457f3 100644
--- a/app/assets/javascripts/ci_variable_list/ajax_variable_list.js
+++ b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js
@@ -47,7 +47,7 @@ export default class AjaxVariableList {
}
onSaveClicked() {
- const loadingIcon = this.saveButton.querySelector('.js-secret-variables-save-loading-icon');
+ const loadingIcon = this.saveButton.querySelector('.js-ci-variables-save-loading-icon');
loadingIcon.classList.toggle('hide', false);
this.errorBox.classList.toggle('hide', true);
// We use this to prevent a user from changing a key before we have a chance
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 1411f7ffd5e..00000000000
--- a/app/assets/javascripts/commons/gitlab_ui.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import Vue from 'vue';
-import {
- Pagination,
- ProgressBar,
- Modal,
- LoadingIcon,
- ModalDirective,
- TooltipDirective,
-} from '@gitlab-org/gitlab-ui';
-
-Vue.component('gl-pagination', Pagination);
-Vue.component('gl-progress-bar', ProgressBar);
-Vue.component('gl-ui-modal', Modal);
-Vue.component('gl-loading-icon', LoadingIcon);
-
-Vue.directive('gl-modal', ModalDirective);
-Vue.directive('gl-tooltip', TooltipDirective);
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..4acd21376bf 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/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..1d2ac59e20a 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/ui';
export default {
components: {
KeysPanel,
NavigationTabs,
+ GlLoadingIcon,
},
props: {
endpoint: {
diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
index c0c21416275..8542a6e718a 100644
--- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
@@ -112,7 +112,7 @@ const JumpToDiscussion = Vue.extend({
if (!hasDiscussionsToJumpTo) {
// If there are no discussions to jump to on the current page,
- // switch to the notes tab and jump to the first disucssion there.
+ // switch to the notes tab and jump to the first discussion there.
window.mrTabs.activateTab('show');
activeTab = 'show';
jumpToFirstDiscussion = true;
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index edca45f22f9..881febedb7c 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/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: {
@@ -41,6 +43,11 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ assignedDiscussions: false,
+ };
+ },
computed: {
...mapState({
isLoading: state => state.diffs.isLoading,
@@ -58,9 +65,9 @@ export default {
plainDiffPath: state => state.diffs.plainDiffPath,
emailPatchPath: state => state.diffs.emailPatchPath,
}),
- ...mapState('diffs', ['showTreeList']),
+ ...mapState('diffs', ['showTreeList', 'isLoading']),
...mapGetters('diffs', ['isParallelView']),
- ...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']),
+ ...mapGetters(['isNotesFetched', 'getNoteableData']),
targetBranch() {
return {
branchName: this.targetBranchName,
@@ -87,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;
@@ -121,6 +128,7 @@ export default {
eventHub.$once('fetchedNotesData', this.setDiscussions);
},
methods: {
+ ...mapActions(['startTaskList']),
...mapActions('diffs', [
'setBaseConfig',
'fetchDiffFiles',
@@ -147,11 +155,14 @@ export default {
}
},
setDiscussions() {
- if (this.isNotesFetched) {
+ if (this.isNotesFetched && !this.assignedDiscussions && !this.isLoading) {
+ this.assignedDiscussions = true;
+
requestIdleCallback(
- () => {
- this.assignDiscussionsToDiff(this.discussionsStructuredByLineCode);
- },
+ () =>
+ this.assignDiscussionsToDiff()
+ .then(this.$nextTick)
+ .then(this.startTaskList),
{ timeout: 1000 },
);
}
@@ -221,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 9bbf62c0eb6..f50cb94a18f 100644
--- a/app/assets/javascripts/diffs/components/compare_versions.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -1,6 +1,6 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
-import Tooltip from '@gitlab-org/gitlab-ui/dist/directives/tooltip';
+import Tooltip from '@gitlab/ui/dist/directives/tooltip';
import { __ } from '~/locale';
import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility';
import Icon from '~/vue_shared/components/icon.vue';
@@ -36,21 +36,18 @@ export default {
},
computed: {
...mapState('diffs', ['commit', 'showTreeList']),
- ...mapGetters('diffs', ['isInlineView', 'isParallelView', 'areAllFilesCollapsed']),
+ ...mapGetters('diffs', ['isInlineView', 'isParallelView', 'hasCollapsedFile']),
comparableDiffs() {
return this.mergeRequestDiffs.slice(1);
},
- isWhitespaceVisible() {
- return !getParameterValues('w')[0];
- },
toggleWhitespaceText() {
- if (this.isWhitespaceVisible) {
+ if (this.isWhitespaceVisible()) {
return __('Hide whitespace changes');
}
return __('Show whitespace changes');
},
toggleWhitespacePath() {
- if (this.isWhitespaceVisible) {
+ if (this.isWhitespaceVisible()) {
return mergeUrlParams({ w: 1 }, window.location.href);
}
@@ -67,6 +64,9 @@ export default {
'expandAllFiles',
'toggleShowTreeList',
]),
+ isWhitespaceVisible() {
+ return getParameterValues('w')[0] !== '1';
+ },
},
};
</script>
@@ -113,15 +113,15 @@ 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') }}
</a>
<a
:href="toggleWhitespacePath"
- class="btn btn-default"
+ class="btn btn-default qa-toggle-whitespace"
>
{{ toggleWhitespaceText }}
</a>
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 f72c7a84e5c..5da0bfb5bfe 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/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: {
@@ -29,7 +31,8 @@ export default {
},
computed: {
...mapState('diffs', ['currentDiffFileId']),
- ...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']),
+ ...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.discussionsStructuredByLineCode);
+ 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..8f8c2a31c71 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/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 34e836a570a..1f82eeae6cb 100644
--- a/app/assets/javascripts/diffs/components/tree_list.vue
+++ b/app/assets/javascripts/diffs/components/tree_list.vue
@@ -1,6 +1,6 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
-import { TooltipDirective as Tooltip } from '@gitlab-org/gitlab-ui';
+import { GlTooltipDirective } from '@gitlab/ui';
import { convertPermissionToBoolean } from '~/lib/utils/common_utils';
import Icon from '~/vue_shared/components/icon.vue';
import FileRow from '~/vue_shared/components/file_row.vue';
@@ -10,7 +10,7 @@ const treeListStorageKey = 'mr_diff_tree_list';
export default {
directives: {
- Tooltip,
+ GlTooltip: GlTooltipDirective,
},
components: {
Icon,
@@ -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() === '') {
@@ -101,7 +101,7 @@ export default {
class="btn-group prepend-left-8 tree-list-view-toggle"
>
<button
- v-tooltip.hover
+ v-gl-tooltip.hover
:aria-label="__('List view')"
:title="__('List view')"
:class="{
@@ -116,7 +116,7 @@ export default {
/>
</button>
<button
- v-tooltip.hover
+ v-gl-tooltip.hover
:aria-label="__('Tree view')"
:title="__('Tree view')"
:class="{
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 1e0b27b538d..6354d3ce1e6 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -5,7 +5,6 @@ import createFlash from '~/flash';
import { s__ } from '~/locale';
import { handleLocationHash, historyPushState } from '~/lib/utils/common_utils';
import { mergeUrlParams, getLocationHash } from '~/lib/utils/url_utility';
-import { reduceDiscussionsToLineCodes } from '../../notes/stores/utils';
import { getDiffPositionByLineCode, getNoteFormData } from './utils';
import * as types from './mutation_types';
import {
@@ -36,24 +35,23 @@ export const fetchDiffFiles = ({ state, commit }) => {
// This is adding line discussions to the actual lines in the diff tree
// once for parallel and once for inline mode
-export const assignDiscussionsToDiff = ({ state, commit }, allLineDiscussions) => {
+export const assignDiscussionsToDiff = (
+ { commit, state, rootState },
+ discussions = rootState.notes.discussions,
+) => {
const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles);
- Object.values(allLineDiscussions).forEach(discussions => {
- if (discussions.length > 0) {
- const { fileHash } = discussions[0];
- commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, {
- fileHash,
- discussions,
- diffPositionByLineCode,
- });
- }
+ discussions.filter(discussion => discussion.diff_discussion).forEach(discussion => {
+ commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, {
+ discussion,
+ diffPositionByLineCode,
+ });
});
};
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 }) => {
@@ -169,7 +167,7 @@ export const expandAllFiles = ({ commit }) => {
export const toggleFileDiscussions = ({ getters, dispatch }, diff) => {
const discussions = getters.getDiffFileDiscussions(diff);
const shouldCloseAll = getters.diffHasAllExpandedDiscussions(diff);
- const shouldExpandAll = getters.diffHasAllCollpasedDiscussions(diff);
+ const shouldExpandAll = getters.diffHasAllCollapsedDiscussions(diff);
discussions.forEach(discussion => {
const data = { discussionId: discussion.id };
@@ -190,9 +188,9 @@ export const saveDiffDiscussion = ({ dispatch }, { note, formData }) => {
return dispatch('saveNote', postData, { root: true })
.then(result => dispatch('updateDiscussion', result.discussion, { root: true }))
- .then(discussion =>
- dispatch('assignDiscussionsToDiff', reduceDiscussionsToLineCodes([discussion])),
- )
+ .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')));
};
@@ -214,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..6a87b712b48 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);
@@ -24,11 +23,11 @@ export const diffHasAllExpandedDiscussions = (state, getters) => diff => {
};
/**
- * Checks if the diff has all discussions collpased
+ * Checks if the diff has all discussions collapsed
* @param {Object} diff
* @returns {Boolean}
*/
-export const diffHasAllCollpasedDiscussions = (state, getters) => diff => {
+export const diffHasAllCollapsedDiscussions = (state, getters) => diff => {
const discussions = getters.getDiffFileDiscussions(diff);
return (
@@ -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 0b4485ecdb5..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 });
},
@@ -90,74 +93,90 @@ export default {
}));
},
- [types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, discussions, diffPositionByLineCode }) {
- const selectedFile = state.diffFiles.find(f => f.fileHash === fileHash);
- const firstDiscussion = discussions[0];
- const isDiffDiscussion = firstDiscussion.diff_discussion;
- const hasLineCode = firstDiscussion.line_code;
- const diffPosition = diffPositionByLineCode[firstDiscussion.line_code];
+ [types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode }) {
+ const { latestDiff } = state;
- if (
- selectedFile &&
- isDiffDiscussion &&
- hasLineCode &&
- diffPosition &&
+ const discussionLineCode = discussion.line_code;
+ const fileHash = discussion.diff_file.file_hash;
+ const lineCheck = line =>
+ line.line_code === discussionLineCode &&
isDiscussionApplicableToLine({
- discussion: firstDiscussion,
- diffPosition,
- latestDiff: state.latestDiff,
- })
- ) {
- const targetLine = selectedFile.parallelDiffLines.find(
- line =>
- (line.left && line.left.lineCode === firstDiscussion.line_code) ||
- (line.right && line.right.lineCode === firstDiscussion.line_code),
- );
- if (targetLine) {
- if (targetLine.left && targetLine.left.lineCode === firstDiscussion.line_code) {
- Object.assign(targetLine.left, {
- discussions,
- });
- } else {
- Object.assign(targetLine.right, {
- discussions,
+ discussion,
+ diffPosition: diffPositionByLineCode[line.line_code],
+ latestDiff,
+ });
+
+ state.diffFiles = state.diffFiles.map(diffFile => {
+ if (diffFile.file_hash === fileHash) {
+ const file = { ...diffFile };
+
+ if (file.highlighted_diff_lines) {
+ file.highlighted_diff_lines = file.highlighted_diff_lines.map(line => {
+ if (lineCheck(line)) {
+ return {
+ ...line,
+ discussions: line.discussions.concat(discussion),
+ };
+ }
+
+ return line;
});
}
- }
-
- if (selectedFile.highlightedDiffLines) {
- const targetInlineLine = selectedFile.highlightedDiffLines.find(
- line => line.lineCode === firstDiscussion.line_code,
- );
- if (targetInlineLine) {
- Object.assign(targetInlineLine, {
- discussions,
+ 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);
+
+ if (left || right) {
+ return {
+ left: {
+ ...line.left,
+ discussions: left ? line.left.discussions.concat(discussion) : [],
+ },
+ right: {
+ ...line.right,
+ discussions: right && !left ? line.right.discussions.concat(discussion) : [],
+ },
+ };
+ }
+
+ return line;
});
}
+
+ if (!file.parallel_diff_lines || !file.highlighted_diff_lines) {
+ file.discussions = file.discussions.concat(discussion);
+ }
+
+ return file;
}
- }
+
+ return diffFile;
+ });
},
- [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) {
@@ -166,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) {
@@ -177,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/due_date_select.js b/app/assets/javascripts/due_date_select.js
index c7b5a35cc14..dbfcf8cc921 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -3,8 +3,7 @@ import Pikaday from 'pikaday';
import dateFormat from 'dateformat';
import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
-import { timeFor } from './lib/utils/datetime_utility';
-import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
+import { timeFor, parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility';
import boardsStore from './boards/stores/boards_store';
class DueDateSelect {
diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue
index 00d197d294f..6c0b5c1f427 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/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..b849772a82e 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/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..af537cfb991 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/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..34d94490533 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/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 a0797b594cb..ae4f07a71cd 100644
--- a/app/assets/javascripts/environments/components/environment_monitoring.vue
+++ b/app/assets/javascripts/environments/components/environment_monitoring.vue
@@ -2,17 +2,16 @@
/**
* Renders the Monitoring (Metrics) link in environments table.
*/
-import { Button } from '@gitlab-org/gitlab-ui';
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
-import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: {
Icon,
- 'gl-button': Button,
+ 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..46c77dca871 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/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..efecc128368 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/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..83727caad16 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/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..4eae4eec394 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/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..2ebc7b5b951 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/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_emoji.js b/app/assets/javascripts/filtered_search/dropdown_emoji.js
index af7936a92fb..d9a4d06b549 100644
--- a/app/assets/javascripts/filtered_search/dropdown_emoji.js
+++ b/app/assets/javascripts/filtered_search/dropdown_emoji.js
@@ -69,10 +69,13 @@ export default class DropdownEmoji extends FilteredSearchDropdown {
// Replace empty gl-emoji tag to real content
const dropdownItems = [...this.dropdown.querySelectorAll('.filter-dropdown-item')];
dropdownItems.forEach(dropdownItem => {
- const name = dropdownItem.querySelector('.js-data-value').innerText;
- const emojiTag = this.glEmojiTag(name);
- const emojiElement = dropdownItem.querySelector('gl-emoji');
- emojiElement.outerHTML = emojiTag;
+ const valueElement = dropdownItem.querySelector('.js-data-value');
+ if (valueElement !== null) {
+ const name = valueElement.innerText;
+ const emojiTag = this.glEmojiTag(name);
+ const emojiElement = dropdownItem.querySelector('gl-emoji');
+ emojiElement.outerHTML = emojiTag;
+ }
});
}
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 b70125c80ca..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
@@ -58,17 +58,22 @@ export const alternativeTokenKeys = [
export const conditions = [
{
- url: 'assignee_id=0',
+ url: 'assignee_id=None',
tokenKey: 'assignee',
value: 'none',
},
{
- url: 'milestone_title=No+Milestone',
+ url: 'assignee_id=Any',
+ tokenKey: 'assignee',
+ value: 'any',
+ },
+ {
+ url: 'milestone_title=None',
tokenKey: 'milestone',
value: 'none',
},
{
- url: 'milestone_title=Any+Milestone',
+ url: 'milestone_title=Any',
tokenKey: 'milestone',
value: 'any',
},
@@ -87,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..1ed8254bc58 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/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 95636a9ccdd..6f8b73564d0 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(' ')}`;
@@ -151,10 +151,16 @@ class GfmAutoComplete {
// Team Members
$input.atwho({
at: '@',
+ alias: 'users',
displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template;
- if (value.username != null) {
- tmpl = GfmAutoComplete.Members.template;
+ const { avatarTag, username, title } = value;
+ if (username != null) {
+ tmpl = GfmAutoComplete.Members.templateFunction({
+ avatarTag,
+ username,
+ title,
+ });
}
return tmpl;
},
@@ -167,7 +173,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 +184,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 {
@@ -201,7 +209,7 @@ class GfmAutoComplete {
displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template;
if (value.title != null) {
- tmpl = GfmAutoComplete.Issues.template;
+ tmpl = GfmAutoComplete.Issues.templateFunction(value.id, value.title);
}
return tmpl;
},
@@ -211,7 +219,7 @@ class GfmAutoComplete {
callbacks: {
...this.getDefaultCallbacks(),
beforeSave(issues) {
- return $.map(issues, (i) => {
+ return $.map(issues, i => {
if (i.title == null) {
return i;
}
@@ -244,7 +252,7 @@ class GfmAutoComplete {
callbacks: {
...this.getDefaultCallbacks(),
beforeSave(milestones) {
- return $.map(milestones, (m) => {
+ return $.map(milestones, m => {
if (m.title == null) {
return m;
}
@@ -267,7 +275,7 @@ class GfmAutoComplete {
displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template;
if (value.title != null) {
- tmpl = GfmAutoComplete.Issues.template;
+ tmpl = GfmAutoComplete.Issues.templateFunction(value.id, value.title);
}
return tmpl;
},
@@ -277,7 +285,7 @@ class GfmAutoComplete {
callbacks: {
...this.getDefaultCallbacks(),
beforeSave(merges) {
- return $.map(merges, (m) => {
+ return $.map(merges, m => {
if (m.title == null) {
return m;
}
@@ -324,13 +332,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;
});
@@ -370,7 +385,7 @@ class GfmAutoComplete {
displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template;
if (value.title != null) {
- tmpl = GfmAutoComplete.Issues.template;
+ tmpl = GfmAutoComplete.Issues.templateFunction(value.id, value.title);
}
return tmpl;
},
@@ -380,7 +395,7 @@ class GfmAutoComplete {
callbacks: {
...this.getDefaultCallbacks(),
beforeSave(snippets) {
- return $.map(snippets, (m) => {
+ return $.map(snippets, m => {
if (m.title == null) {
return m;
}
@@ -458,13 +473,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 +516,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 +533,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);
}
@@ -548,17 +571,20 @@ GfmAutoComplete.Emoji = {
};
// Team Members
GfmAutoComplete.Members = {
- // eslint-disable-next-line no-template-curly-in-string
- template: '<li>${avatarTag} ${username} <small>${title}</small></li>',
+ templateFunction({ avatarTag, username, title }) {
+ return `<li>${avatarTag} ${username} <small>${_.escape(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 = {
- // eslint-disable-next-line no-template-curly-in-string
- template: '<li><small>${id}</small> ${title}</li>',
+ templateFunction(id, title) {
+ return `<li><small>${id}</small> ${_.escape(title)}</li>`;
+ },
};
// Milestones
GfmAutoComplete.Milestones = {
@@ -566,7 +592,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..29dc2d6a8a3 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/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..600e1063f89 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/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..11fc15871ac 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/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..81d8d0b5132 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/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 dc84ee12f1e..6b4849eb6f9 100644
--- a/app/assets/javascripts/ide/components/ide_side_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_side_bar.vue
@@ -1,6 +1,6 @@
<script>
import { mapState, mapGetters } from 'vuex';
-import { SkeletonLoading } from '@gitlab-org/gitlab-ui';
+import { GlSkeletonLoading } from '@gitlab/ui';
import IdeTree from './ide_tree.vue';
import ResizablePanel from './resizable_panel.vue';
import ActivityBar from './activity_bar.vue';
@@ -13,7 +13,7 @@ import { activityBarViews } from '../constants';
export default {
components: {
- SkeletonLoading,
+ GlSkeletonLoading,
ResizablePanel,
ActivityBar,
CommitSection,
@@ -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)
);
},
},
@@ -50,7 +50,7 @@ export default {
:key="n"
class="multi-file-loading-container"
>
- <skeleton-loading />
+ <gl-skeleton-loading />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue
index e88f01fb4f4..12ed7f86b3d 100644
--- a/app/assets/javascripts/ide/components/ide_tree_list.vue
+++ b/app/assets/javascripts/ide/components/ide_tree_list.vue
@@ -1,7 +1,7 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
-import { SkeletonLoading } from '@gitlab-org/gitlab-ui';
+import { GlSkeletonLoading } from '@gitlab/ui';
import FileRow from '~/vue_shared/components/file_row.vue';
import NavDropdown from './nav_dropdown.vue';
import FileRowExtra from './file_row_extra.vue';
@@ -9,7 +9,7 @@ import FileRowExtra from './file_row_extra.vue';
export default {
components: {
Icon,
- SkeletonLoading,
+ GlSkeletonLoading,
NavDropdown,
FileRow,
},
@@ -51,7 +51,7 @@ export default {
:key="n"
class="multi-file-loading-container"
>
- <skeleton-loading />
+ <gl-skeleton-loading />
</div>
</template>
<template v-else>
diff --git a/app/assets/javascripts/ide/components/jobs/list.vue b/app/assets/javascripts/ide/components/jobs/list.vue
index acd37605d16..e3626f60899 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/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..e4fc78afaf2 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/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..4df29590b85 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/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 0a2681b7a1e..36cfcac4186 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/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']),
@@ -25,7 +27,7 @@ export default {
...mapState('pipelines', ['isLoadingPipeline', 'latestPipeline', 'stages', 'isLoadingJobs']),
ciLintText() {
return sprintf(
- __('You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}'),
+ __('You can test your .gitlab-ci.yml in %{linkStart}CI Lint%{linkEnd}.'),
{
linkStart: `<a href="${_.escape(this.currentProject.web_url)}/-/ci/lint">`,
linkEnd: '</a>',
diff --git a/app/assets/javascripts/ide/components/preview/clientside.vue b/app/assets/javascripts/ide/components/preview/clientside.vue
index 37a8ad36507..afc0cfca7d8 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/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..cc6fc595b74 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/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/ide/stores/modules/pipelines/actions.js b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
index 8fa86995ef0..ea2525a2f0e 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
@@ -28,7 +28,7 @@ export const receiveLatestPipelineError = ({ commit, dispatch }, err) => {
dispatch(
'setErrorMessage',
{
- text: __('An error occured whilst fetching the latest pipline.'),
+ text: __('An error occured whilst fetching the latest pipeline.'),
action: () =>
dispatch('forcePipelineRequest').then(() =>
dispatch('setErrorMessage', null, { root: true }),
diff --git a/app/assets/javascripts/init_legacy_filters.js b/app/assets/javascripts/init_legacy_filters.js
deleted file mode 100644
index b6ff97d1279..00000000000
--- a/app/assets/javascripts/init_legacy_filters.js
+++ /dev/null
@@ -1,14 +0,0 @@
-/* eslint-disable no-new */
-import LabelsSelect from './labels_select';
-import subscriptionSelect from './subscription_select';
-import UsersSelect from './users_select';
-import issueStatusSelect from './issue_status_select';
-import MilestoneSelect from './milestone_select';
-
-export default () => {
- new UsersSelect();
- new LabelsSelect();
- new MilestoneSelect();
- issueStatusSelect();
- subscriptionSelect();
-};
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 0140960b367..c81a2230310 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -1,6 +1,3 @@
-/* eslint-disable no-new, no-unused-vars, consistent-return, no-else-return */
-/* global GitLab */
-
import $ from 'jquery';
import Pikaday from 'pikaday';
import Autosave from './autosave';
@@ -8,7 +5,7 @@ import UsersSelect from './users_select';
import GfmAutoComplete from './gfm_auto_complete';
import ZenMode from './zen_mode';
import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select';
-import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
+import { parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility';
export default class IssuableForm {
constructor(form) {
@@ -19,9 +16,11 @@ export default class IssuableForm {
this.handleSubmit = this.handleSubmit.bind(this);
this.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i;
- new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup();
- new UsersSelect();
- new ZenMode();
+ this.gfmAutoComplete = new GfmAutoComplete(
+ gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources,
+ ).setup();
+ this.usersSelect = new UsersSelect();
+ this.zenMode = new ZenMode();
this.titleField = this.form.find('input[name*="[title]"]');
this.descriptionField = this.form.find('textarea[name*="[description]"]');
@@ -57,8 +56,16 @@ export default class IssuableForm {
}
initAutosave() {
- new Autosave(this.titleField, [document.location.pathname, document.location.search, 'title']);
- return new Autosave(this.descriptionField, [document.location.pathname, document.location.search, 'description']);
+ this.autosave = new Autosave(this.titleField, [
+ document.location.pathname,
+ document.location.search,
+ 'title',
+ ]);
+ return new Autosave(this.descriptionField, [
+ document.location.pathname,
+ document.location.search,
+ 'description',
+ ]);
}
handleSubmit() {
@@ -74,7 +81,7 @@ export default class IssuableForm {
this.$wipExplanation = this.form.find('.js-wip-explanation');
this.$noWipExplanation = this.form.find('.js-no-wip-explanation');
if (!(this.$wipExplanation.length && this.$noWipExplanation.length)) {
- return;
+ return undefined;
}
this.form.on('click', '.js-toggle-wip', this.toggleWip);
this.titleField.on('keyup blur', this.renderWipExplanation);
@@ -89,10 +96,9 @@ export default class IssuableForm {
if (this.workInProgress()) {
this.$wipExplanation.show();
return this.$noWipExplanation.hide();
- } else {
- this.$wipExplanation.hide();
- return this.$noWipExplanation.show();
}
+ this.$wipExplanation.hide();
+ return this.$noWipExplanation.show();
}
toggleWip(event) {
@@ -110,7 +116,7 @@ export default class IssuableForm {
}
addWip() {
- this.titleField.val(`WIP: ${(this.titleField.val())}`);
+ this.titleField.val(`WIP: ${this.titleField.val()}`);
}
initTargetBranchDropdown() {
diff --git a/app/assets/javascripts/jobs/components/artifacts_block.vue b/app/assets/javascripts/jobs/components/artifacts_block.vue
index 17fd5321642..932675c0fc6 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/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..9373dbebc8a 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/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..afc4d931a68 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/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..712f564b065 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/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 ba14aaeed2c..ecb809ca4de 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/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',
- 'isJobStuck',
- 'hasTrace',
- 'emptyStateIllustration',
- 'isScrollingDown',
- 'emptyStateAction',
- ]),
-
- 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>
@@ -195,9 +219,9 @@
<!-- Body Section -->
<stuck-block
- v-if="isJobStuck"
+ v-if="job.stuck"
class="js-job-stuck"
- :has-no-runners-for-project="job.runners.available"
+ :has-no-runners-for-project="hasRunnersForProject"
:tags="job.tags"
:runners-path="runnerSettingsUrl"
/>
@@ -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..80cbed422a0 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/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..5e0495bb231 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/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 906769ee6a2..21f0a1435d7 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/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: {
@@ -31,7 +34,7 @@ export default {
},
},
computed: {
- ...mapState(['job', 'stages', 'jobs', 'selectedStage']),
+ ...mapState(['job', 'stages', 'jobs', 'selectedStage', 'isLoadingStages']),
coverage() {
return `${this.job.coverage}%`;
},
@@ -59,10 +62,10 @@ export default {
return '';
}
- let t = this.job.metadata.timeout_human_readable;
- if (this.job.metadata.timeout_source !== '') {
- t += ` (from ${this.job.metadata.timeout_source})`;
- }
+ let t = this.job.metadata.timeout_human_readable;
+ if (this.job.metadata.timeout_source !== '') {
+ t += ` (from ${this.job.metadata.timeout_source})`;
+ }
return t;
},
@@ -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>
@@ -270,6 +273,7 @@ export default {
/>
<stages-dropdown
+ v-if="!isLoadingStages"
:stages="stages"
:pipeline="job.pipeline"
:selected-stage="selectedStage"
diff --git a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
index aeafe98a70b..d143e9f586c 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/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/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue
index e5e1d56e287..dc26b246d71 100644
--- a/app/assets/javascripts/jobs/components/stages_dropdown.vue
+++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue
@@ -22,7 +22,6 @@ export default {
required: true,
},
},
-
computed: {
hasRef() {
return !_.isEmpty(this.pipeline.ref);
diff --git a/app/assets/javascripts/jobs/components/stuck_block.vue b/app/assets/javascripts/jobs/components/stuck_block.vue
index a60643b2c65..2b2ebe4c3f7 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/ui';
/**
* Renders Stuck Runners block for job's view.
*/
export default {
+ components: {
+ GlLink,
+ },
props: {
hasNoRunnersForProject: {
type: Boolean,
@@ -23,14 +27,7 @@ export default {
<template>
<div class="bs-callout bs-callout-warning">
<p
- v-if="hasNoRunnersForProject"
- class="js-stuck-no-runners append-bottom-0"
- >
- {{ s__(`Job|This job is stuck, because the project
- doesn't have any runners online assigned to it.`) }}
- </p>
- <p
- v-else-if="tags.length"
+ v-if="tags.length"
class="js-stuck-with-tags append-bottom-0"
>
{{ s__(`This job is stuck, because you don't have
@@ -44,6 +41,13 @@ export default {
</span>
</p>
<p
+ v-else-if="hasNoRunnersForProject"
+ class="js-stuck-no-runners append-bottom-0"
+ >
+ {{ s__(`Job|This job is stuck, because the project
+ doesn't have any runners online assigned to it.`) }}
+ </p>
+ <p
v-else
class="js-stuck-no-active-runner append-bottom-0"
>
@@ -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..4d18f76b7ea 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/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 4ce395a9106..35e92b0b5d9 100644
--- a/app/assets/javascripts/jobs/store/getters.js
+++ b/app/assets/javascripts/jobs/store/getters.js
@@ -35,23 +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) || {};
-/**
- * When the job is pending and there are no available runners
- * we need to render the stuck block;
- *
- * @returns {Boolean}
- */
-export const isJobStuck = state =>
- (!_.isEmpty(state.job.status) && state.job.status.group === 'pending') &&
- (!_.isEmpty(state.job.runners) && state.job.runners.available === false);
+export const emptyStateAction = state =>
+ (state.job && state.job.status && state.job.status.action) || null;
export const isScrollingDown = state => isScrolledToBottom() && !state.isTraceComplete;
+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/jobs/store/mutations.js b/app/assets/javascripts/jobs/store/mutations.js
index 4195d787f12..cd440d21c1f 100644
--- a/app/assets/javascripts/jobs/store/mutations.js
+++ b/app/assets/javascripts/jobs/store/mutations.js
@@ -71,7 +71,7 @@ export default {
* after the first request,
* and we do not want to hijack that
*/
- if (state.selectedStage === 'More' && job.stage) {
+ if (state.selectedStage === '' && job.stage) {
state.selectedStage = job.stage;
}
},
diff --git a/app/assets/javascripts/jobs/store/state.js b/app/assets/javascripts/jobs/store/state.js
index 0eb269ca38f..04825187c99 100644
--- a/app/assets/javascripts/jobs/store/state.js
+++ b/app/assets/javascripts/jobs/store/state.js
@@ -1,5 +1,3 @@
-import { __ } from '~/locale';
-
export default () => ({
jobEndpoint: null,
traceEndpoint: null,
@@ -29,7 +27,7 @@ export default () => ({
// sidebar dropdown & list of jobs
isLoadingStages: false,
isLoadingJobs: false,
- selectedStage: __('More'),
+ selectedStage: '',
stages: [],
jobs: [],
});
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/datefix.js b/app/assets/javascripts/lib/utils/datefix.js
deleted file mode 100644
index 19e4085dbbb..00000000000
--- a/app/assets/javascripts/lib/utils/datefix.js
+++ /dev/null
@@ -1,28 +0,0 @@
-export const pad = (val, len = 2) => `0${val}`.slice(-len);
-
-/**
- * Formats dates in Pickaday
- * @param {String} dateString Date in yyyy-mm-dd format
- * @return {Date} UTC format
- */
-export const parsePikadayDate = dateString => {
- const parts = dateString.split('-');
- const year = parseInt(parts[0], 10);
- const month = parseInt(parts[1] - 1, 10);
- const day = parseInt(parts[2], 10);
-
- return new Date(year, month, day);
-};
-
-/**
- * Used `onSelect` method in pickaday
- * @param {Date} date UTC format
- * @return {String} Date formated in yyyy-mm-dd
- */
-export const pikadayToString = date => {
- const day = pad(date.getDate());
- const month = pad(date.getMonth() + 1);
- const year = date.getFullYear();
-
- return `${year}-${month}-${day}`;
-};
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 833dbefd3dc..59007d5950e 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import _ from 'underscore';
import timeago from 'timeago.js';
import dateFormat from 'dateformat';
import { pluralize } from './text_utility';
@@ -13,7 +14,7 @@ window.timeago = timeago;
*
* @param {Boolean} abbreviated
*/
-const getMonthNames = abbreviated => {
+export const getMonthNames = abbreviated => {
if (abbreviated) {
return [
s__('Jan'),
@@ -46,6 +47,8 @@ const getMonthNames = abbreviated => {
];
};
+export const pad = (val, len = 2) => `0${val}`.slice(-len);
+
/**
* Given a date object returns the day of the week in English
* @param {date} date
@@ -74,10 +77,10 @@ let timeagoInstance;
/**
* Sets a timeago Instance
*/
-export function getTimeago() {
+export const getTimeago = () => {
if (!timeagoInstance) {
- const localeRemaining = function getLocaleRemaining(number, index) {
- return [
+ const localeRemaining = (number, index) =>
+ [
[s__('Timeago|just now'), s__('Timeago|right now')],
[s__('Timeago|%s seconds ago'), s__('Timeago|%s seconds remaining')],
[s__('Timeago|1 minute ago'), s__('Timeago|1 minute remaining')],
@@ -93,9 +96,9 @@ export function getTimeago() {
[s__('Timeago|1 year ago'), s__('Timeago|1 year remaining')],
[s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')],
][index];
- };
- const locale = function getLocale(number, index) {
- return [
+
+ const locale = (number, index) =>
+ [
[s__('Timeago|just now'), s__('Timeago|right now')],
[s__('Timeago|%s seconds ago'), s__('Timeago|in %s seconds')],
[s__('Timeago|1 minute ago'), s__('Timeago|in 1 minute')],
@@ -111,7 +114,6 @@ export function getTimeago() {
[s__('Timeago|1 year ago'), s__('Timeago|in 1 year')],
[s__('Timeago|%s years ago'), s__('Timeago|in %s years')],
][index];
- };
timeago.register(timeagoLanguageCode, locale);
timeago.register(`${timeagoLanguageCode}-remaining`, localeRemaining);
@@ -119,7 +121,7 @@ export function getTimeago() {
}
return timeagoInstance;
-}
+};
/**
* For the given element, renders a timeago instance.
@@ -184,7 +186,7 @@ export const getDayDifference = (a, b) => {
* @param {Number} seconds
* @return {String}
*/
-export function timeIntervalInWords(intervalInSeconds) {
+export const timeIntervalInWords = intervalInSeconds => {
const secondsInteger = parseInt(intervalInSeconds, 10);
const minutes = Math.floor(secondsInteger / 60);
const seconds = secondsInteger - minutes * 60;
@@ -196,9 +198,9 @@ export function timeIntervalInWords(intervalInSeconds) {
text = `${seconds} ${pluralize('second', seconds)}`;
}
return text;
-}
+};
-export function dateInWords(date, abbreviated = false, hideYear = false) {
+export const dateInWords = (date, abbreviated = false, hideYear = false) => {
if (!date) return date;
const month = date.getMonth();
@@ -240,7 +242,7 @@ export function dateInWords(date, abbreviated = false, hideYear = false) {
}
return `${monthName} ${date.getDate()}, ${year}`;
-}
+};
/**
* Returns month name based on provided date.
@@ -391,3 +393,103 @@ export const formatTime = milliseconds => {
formattedTime += remainingSeconds;
return formattedTime;
};
+
+/**
+ * Formats dates in Pickaday
+ * @param {String} dateString Date in yyyy-mm-dd format
+ * @return {Date} UTC format
+ */
+export const parsePikadayDate = dateString => {
+ const parts = dateString.split('-');
+ const year = parseInt(parts[0], 10);
+ const month = parseInt(parts[1] - 1, 10);
+ const day = parseInt(parts[2], 10);
+
+ return new Date(year, month, day);
+};
+
+/**
+ * Used `onSelect` method in pickaday
+ * @param {Date} date UTC format
+ * @return {String} Date formated in yyyy-mm-dd
+ */
+export const pikadayToString = date => {
+ const day = pad(date.getDate());
+ const month = pad(date.getMonth() + 1);
+ const year = date.getFullYear();
+
+ return `${year}-${month}-${day}`;
+};
+
+/**
+ * Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # }
+ * Seconds can be negative or positive, zero or non-zero. Can be configured for any day
+ * or week length.
+ */
+export const parseSeconds = (seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {}) => {
+ const DAYS_PER_WEEK = daysPerWeek;
+ const HOURS_PER_DAY = hoursPerDay;
+ const MINUTES_PER_HOUR = 60;
+ const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR;
+ const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR;
+
+ const timePeriodConstraints = {
+ weeks: MINUTES_PER_WEEK,
+ days: MINUTES_PER_DAY,
+ hours: MINUTES_PER_HOUR,
+ minutes: 1,
+ };
+
+ let unorderedMinutes = Math.abs(seconds / MINUTES_PER_HOUR);
+
+ return _.mapObject(timePeriodConstraints, minutesPerPeriod => {
+ const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod);
+
+ unorderedMinutes -= periodCount * minutesPerPeriod;
+
+ return periodCount;
+ });
+};
+
+/**
+ * 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, 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;
+ },
+ '',
+ ).trim();
+ return reducedTime.length ? reducedTime : '0m';
+};
+
+/**
+ * Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns
+ * the first non-zero unit/value pair.
+ */
+export const abbreviateTime = timeStr =>
+ timeStr.split(' ').filter(unitStr => unitStr.charAt(0) !== '0')[0];
+
+/**
+ * Calculates the milliseconds between now and a given date string.
+ * The result cannot become negative.
+ *
+ * @param endDate date string that the time difference is calculated for
+ * @return {number} number of milliseconds remaining until the given date
+ */
+export const calculateRemainingMilliseconds = endDate => {
+ const remainingMilliseconds = new Date(endDate).getTime() - Date.now();
+ return Math.max(remainingMilliseconds, 0);
+};
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/lib/utils/pretty_time.js b/app/assets/javascripts/lib/utils/pretty_time.js
deleted file mode 100644
index d92b8a7179f..00000000000
--- a/app/assets/javascripts/lib/utils/pretty_time.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import _ from 'underscore';
-
-/*
- * TODO: Make these methods more configurable (e.g. stringifyTime condensed or
- * non-condensed, abbreviateTimelengths)
- * */
-
-/*
- * Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # }
- * Seconds can be negative or positive, zero or non-zero. Can be configured for any day
- * or week length.
-*/
-
-export function parseSeconds(seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {}) {
- const DAYS_PER_WEEK = daysPerWeek;
- const HOURS_PER_DAY = hoursPerDay;
- const MINUTES_PER_HOUR = 60;
- const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR;
- const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR;
-
- const timePeriodConstraints = {
- weeks: MINUTES_PER_WEEK,
- days: MINUTES_PER_DAY,
- hours: MINUTES_PER_HOUR,
- minutes: 1,
- };
-
- let unorderedMinutes = Math.abs(seconds / MINUTES_PER_HOUR);
-
- return _.mapObject(timePeriodConstraints, minutesPerPeriod => {
- const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod);
-
- unorderedMinutes -= periodCount * minutesPerPeriod;
-
- return periodCount;
- });
-}
-
-/*
-* 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.
-*/
-
-export function stringifyTime(timeObject) {
- const reducedTime = _.reduce(
- timeObject,
- (memo, unitValue, unitName) => {
- const isNonZero = !!unitValue;
- return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo;
- },
- '',
- ).trim();
- return reducedTime.length ? reducedTime : '0m';
-}
-
-/*
-* Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns
-* the first non-zero unit/value pair.
-*/
-
-export function abbreviateTime(timeStr) {
- return timeStr.split(' ').filter(unitStr => unitStr.charAt(0) !== '0')[0];
-}
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index e26a6b986be..3618c6af7e2 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -2,6 +2,8 @@
import $ from 'jquery';
import { insertText } from '~/lib/utils/common_utils';
+const LINK_TAG_PATTERN = '[{text}](url)';
+
function selectedText(text, textarea) {
return text.substring(textarea.selectionStart, textarea.selectionEnd);
}
@@ -37,7 +39,7 @@ function blockTagText(text, textArea, blockTag, selected) {
}
}
-function moveCursor({ textArea, tag, wrapped, removedLastNewLine, select }) {
+function moveCursor({ textArea, tag, positionBetweenTags, removedLastNewLine, select }) {
var pos;
if (!textArea.setSelectionRange) {
return;
@@ -49,7 +51,7 @@ function moveCursor({ textArea, tag, wrapped, removedLastNewLine, select }) {
return textArea.setSelectionRange(startPosition, endPosition);
}
if (textArea.selectionStart === textArea.selectionEnd) {
- if (wrapped) {
+ if (positionBetweenTags) {
pos = textArea.selectionStart - tag.length;
} else {
pos = textArea.selectionStart;
@@ -65,7 +67,6 @@ function moveCursor({ textArea, tag, wrapped, removedLastNewLine, select }) {
export function insertMarkdownText({ textArea, text, tag, blockTag, selected, wrap, select }) {
var textToInsert,
- inserted,
selectedSplit,
startChar,
removedLastNewLine,
@@ -76,6 +77,21 @@ export function insertMarkdownText({ textArea, text, tag, blockTag, selected, wr
removedFirstNewLine = false;
currentLineEmpty = false;
+ // check for link pattern and selected text is an URL
+ // if so fill in the url part instead of the text part of the pattern.
+ if (tag === LINK_TAG_PATTERN) {
+ if (URL) {
+ try {
+ const ignoredUrl = new URL(selected);
+ // valid url
+ tag = '[text]({text})';
+ select = 'text';
+ } catch (e) {
+ // ignore - no valid url
+ }
+ }
+ }
+
// Remove the first newline
if (selected.indexOf('\n') === 0) {
removedFirstNewLine = true;
@@ -138,7 +154,7 @@ export function insertMarkdownText({ textArea, text, tag, blockTag, selected, wr
return moveCursor({
textArea,
tag: tag.replace(textPlaceholder, selected),
- wrap,
+ positionBetweenTags: wrap && selected.length === 0,
removedLastNewLine,
select,
});
@@ -154,10 +170,6 @@ function updateText({ textArea, tag, blockTag, wrap, select }) {
return insertMarkdownText({ textArea, text, tag, blockTag, selected, wrap, select });
}
-function replaceRange(s, start, end, substitute) {
- return s.substring(0, start) + substitute + s.substring(end);
-}
-
export function addMarkdownListeners(form) {
return $('.js-md', form)
.off('click')
diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js
index df5cd1b8c51..0beedcacf33 100644
--- a/app/assets/javascripts/member_expiration_date.js
+++ b/app/assets/javascripts/member_expiration_date.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import Pikaday from 'pikaday';
-import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
+import { parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility';
// Add datepickers to all `js-access-expiration-date` elements. If those elements are
// children of an element with the `clearable-input` class, and have a sibling
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/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 67338aa96c3..98182d92c2f 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -149,7 +149,7 @@ export default {
.catch(() => Flash(s__('Metrics|There was an error getting deployment information.'))),
this.service
.getEnvironmentsData()
- .then((data) => this.store.storeEnvironmentsData(data))
+ .then(data => this.store.storeEnvironmentsData(data))
.catch(() => Flash(s__('Metrics|There was an error getting environments information.'))),
])
.then(() => {
@@ -157,6 +157,7 @@ export default {
this.state = 'noData';
return;
}
+
this.showEmptyState = false;
})
.then(this.resize)
@@ -195,7 +196,10 @@ export default {
name="chevron-down"
/>
</button>
- <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
+ <div
+ v-if="store.environmentsData.length > 0"
+ class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"
+ >
<ul>
<li
v-for="environment in store.environmentsData"
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
index ed5c8b15945..5c6e2e09e46 100644
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -121,6 +121,7 @@ export default {
draw() {
const breakpointSize = bp.getBreakpointSize();
const query = this.graphData.queries[0];
+ const svgWidth = this.$refs.baseSvg.getBoundingClientRect().width;
this.margin = measurements.large.margin;
if (this.smallGraph || breakpointSize === 'xs' || breakpointSize === 'sm') {
this.graphHeight = 300;
@@ -130,13 +131,13 @@ export default {
this.unitOfDisplay = query.unit || '';
this.yAxisLabel = this.graphData.y_label || 'Values';
this.legendTitle = query.label || 'Average';
- this.graphWidth = this.$refs.baseSvg.clientWidth - this.margin.left - this.margin.right;
+ this.graphWidth = svgWidth - this.margin.left - this.margin.right;
this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
this.baseGraphHeight = this.graphHeight - 50;
this.baseGraphWidth = this.graphWidth;
// pixel offsets inside the svg and outside are not 1:1
- this.realPixelRatio = this.$refs.baseSvg.clientWidth / this.baseGraphWidth;
+ this.realPixelRatio = svgWidth / this.baseGraphWidth;
this.renderAxesPaths();
this.formatDeployments();
diff --git a/app/assets/javascripts/notebook/cells/output/index.vue b/app/assets/javascripts/notebook/cells/output/index.vue
index d9f8604ed10..bd0bcc0d819 100644
--- a/app/assets/javascripts/notebook/cells/output/index.vue
+++ b/app/assets/javascripts/notebook/cells/output/index.vue
@@ -22,7 +22,7 @@ export default {
},
output: {
type: Object,
- requred: true,
+ required: true,
default: () => ({}),
},
},
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 1369b5820d5..dfb53c986fc 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -16,7 +16,7 @@ import 'vendor/jquery.atwho';
import AjaxCache from '~/lib/utils/ajax_cache';
import Vue from 'vue';
import syntaxHighlight from '~/syntax_highlight';
-import { SkeletonLoading } from '@gitlab-org/gitlab-ui';
+import { GlSkeletonLoading } from '@gitlab/ui';
import axios from './lib/utils/axios_utils';
import { getLocationHash } from './lib/utils/url_utility';
import Flash from './flash';
@@ -1293,10 +1293,10 @@ export default class Notes {
new Vue({
el,
components: {
- SkeletonLoading,
+ GlSkeletonLoading,
},
render(createElement) {
- return createElement('skeleton-loading');
+ return createElement('gl-skeleton-loading');
},
});
}
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 d9e99603238..30fcb895369 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 { SkeletonLoading } from '@gitlab-org/gitlab-ui';
-import { trimFirstCharOfLineContent } from '~/diffs/store/utils';
+import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
+import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
+import { GlSkeletonLoading } from '@gitlab/ui';
+import { trimFirstCharOfLineContent, getDiffMode } from '~/diffs/store/utils';
export default {
components: {
DiffFileHeader,
- SkeletonLoading,
+ 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>
@@ -143,7 +145,7 @@ export default {
class="line_content js-success-lazy-load"
>
<span></span>
- <skeleton-loading />
+ <gl-skeleton-loading />
<span></span>
</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 27972682ca1..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,26 +13,30 @@ 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',
- ]),
+ ...mapGetters(['getNotesDataByProp']),
currentFilter() {
if (!this.currentValue) return this.filters[0];
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);
@@ -41,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);
},
},
};
@@ -51,7 +60,7 @@ export default {
<button
id="discussion-filter-dropdown"
ref="dropdownToggle"
- class="btn btn-default"
+ class="btn btn-default qa-discussion-filter"
data-toggle="dropdown"
aria-expanded="false"
>
@@ -69,11 +78,16 @@ export default {
>
<button
:class="{ 'is-active': filter.value === currentValue }"
+ class="qa-filter-options"
type="button"
@click="selectFilter(filter.value)"
>
{{ 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..0f0d7f4661a 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/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 c68860d98ae..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) {
@@ -95,31 +91,22 @@ export default {
return awardList.filter(award => award.user.id === this.getUserData.id).length;
},
awardTitle(awardsList) {
- const hasReactionByCurrentUser = this.hasReactionByCurrentUser(
- awardsList,
- );
+ const hasReactionByCurrentUser = this.hasReactionByCurrentUser(awardsList);
const TOOLTIP_NAME_COUNT = hasReactionByCurrentUser ? 9 : 10;
let awardList = awardsList;
// Filter myself from list if I am awarded.
if (hasReactionByCurrentUser) {
- awardList = awardList.filter(
- award => award.user.id !== this.getUserData.id,
- );
+ awardList = awardList.filter(award => award.user.id !== this.getUserData.id);
}
// Get only 9-10 usernames to show in tooltip text.
- const namesToShow = awardList
- .slice(0, TOOLTIP_NAME_COUNT)
- .map(award => award.user.name);
+ const namesToShow = awardList.slice(0, TOOLTIP_NAME_COUNT).map(award => award.user.name);
// Get the remaining list to use in `and x more` text.
- const remainingAwardList = awardList.slice(
- TOOLTIP_NAME_COUNT,
- awardList.length,
- );
+ 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');
}
@@ -128,9 +115,7 @@ export default {
// We have 10+ awarded user, join them with comma and add `and x more`.
if (remainingAwardList.length) {
- title = `${namesToShow.join(', ')}, and ${
- remainingAwardList.length
- } more.`;
+ title = `${namesToShow.join(', ')}, and ${remainingAwardList.length} more.`;
} else if (namesToShow.length > 1) {
// Join all names with comma but not the last one, it will be added with and text.
title = namesToShow.slice(0, namesToShow.length - 1).join(', ');
@@ -170,9 +155,7 @@ export default {
awardName: parsedName,
};
- this.toggleAwardRequest(data).catch(() =>
- Flash('Something went wrong on our end.'),
- );
+ this.toggleAwardRequest(data).catch(() => Flash('Something went wrong on our end.'));
},
},
};
@@ -209,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 fa4a1c56b20..bee6d4f0329 100644
--- a/app/assets/javascripts/notes/stores/collapse_utils.js
+++ b/app/assets/javascripts/notes/stores/collapse_utils.js
@@ -68,12 +68,9 @@ export const collapseSystemNotes = notes => {
lastDescriptionSystemNote = note;
lastDescriptionSystemNoteIndex = acc.length;
} else if (lastDescriptionSystemNote) {
- const timeDifferenceMinutes = getTimeDifferenceMinutes(
- lastDescriptionSystemNote,
- note,
- );
+ 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 21c334a9d33..8df95c279eb 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -1,6 +1,5 @@
import _ from 'underscore';
import * as constants from '../constants';
-import { reduceDiscussionsToLineCodes } from './utils';
import { collapseSystemNotes } from './collapse_utils';
export const discussions = state => collapseSystemNotes(state.discussions);
@@ -31,9 +30,6 @@ export const notesById = state =>
return acc;
}, {});
-export const discussionsStructuredByLineCode = state =>
- reduceDiscussionsToLineCodes(state.discussions);
-
export const noteableType = state => {
const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE, EPIC_NOTEABLE_TYPE } = constants;
@@ -196,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/index.js b/app/assets/javascripts/notes/stores/index.js
index f105b7d0d11..d41b02b4a4b 100644
--- a/app/assets/javascripts/notes/stores/index.js
+++ b/app/assets/javascripts/notes/stores/index.js
@@ -4,5 +4,4 @@ import notesModule from './modules';
Vue.use(Vuex);
-export default () =>
- new Vuex.Store(notesModule());
+export default () => new Vuex.Store(notesModule());
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/notes/stores/utils.js b/app/assets/javascripts/notes/stores/utils.js
index 0e41ff03d67..dd57539e4d8 100644
--- a/app/assets/javascripts/notes/stores/utils.js
+++ b/app/assets/javascripts/notes/stores/utils.js
@@ -25,18 +25,6 @@ export const getQuickActionText = note => {
return text;
};
-export const reduceDiscussionsToLineCodes = selectedDiscussions =>
- selectedDiscussions.reduce((acc, note) => {
- if (note.diff_discussion && note.line_code) {
- // For context about line notes: there might be multiple notes with the same line code
- const items = acc[note.line_code] || [];
- items.push(note);
-
- Object.assign(acc, { [note.line_code]: items });
- }
- return acc;
- }, {});
-
export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note);
export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim();
diff --git a/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js b/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js
index 15e737fff05..d9cf62db3f7 100644
--- a/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js
+++ b/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js
@@ -31,7 +31,7 @@ export default class AbuseReports {
$messageCellElement.text(originalMessage);
} else {
$messageCellElement.data('messageTruncated', 'true');
- $messageCellElement.text(`${originalMessage.substr(0, (MAX_MESSAGE_LENGTH - 3))}...`);
+ $messageCellElement.text(`${originalMessage.substr(0, MAX_MESSAGE_LENGTH - 3)}...`);
}
}
}
diff --git a/app/assets/javascripts/pages/admin/admin.js b/app/assets/javascripts/pages/admin/admin.js
index ff4d6ab15f9..4616a075729 100644
--- a/app/assets/javascripts/pages/admin/admin.js
+++ b/app/assets/javascripts/pages/admin/admin.js
@@ -23,7 +23,7 @@ export default function adminInit() {
}
});
- $('body').on('click', '.js-toggle-colors-link', (e) => {
+ $('body').on('click', '.js-toggle-colors-link', e => {
e.preventDefault();
$('.js-toggle-colors-container').toggleClass('hide');
});
@@ -33,12 +33,15 @@ export default function adminInit() {
$(this).tab('show');
});
- $('.log-bottom').on('click', (e) => {
+ $('.log-bottom').on('click', e => {
e.preventDefault();
const $visibleLog = $('.file-content:visible');
- $visibleLog.animate({
- scrollTop: $visibleLog.find('ol').height(),
- }, 'fast');
+ $visibleLog.animate(
+ {
+ scrollTop: $visibleLog.find('ol').height(),
+ },
+ 'fast',
+ );
});
$('.change-owner-link').on('click', function changeOwnerLinkClick(e) {
@@ -47,7 +50,7 @@ export default function adminInit() {
modal.show();
});
- $('.change-owner-cancel-link').on('click', (e) => {
+ $('.change-owner-cancel-link').on('click', e => {
e.preventDefault();
modal.hide();
$('.change-owner-link').show();
diff --git a/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js b/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js
index 7281f907ec7..455c637a6b3 100644
--- a/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js
+++ b/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js
@@ -1,10 +1,14 @@
import { __ } from '~/locale';
export const PLACEHOLDER_USER_EXTERNAL_DEFAULT_TRUE = __('Regex pattern');
-export const PLACEHOLDER_USER_EXTERNAL_DEFAULT_FALSE = __('To define internal users, first enable new users set to external');
+export const PLACEHOLDER_USER_EXTERNAL_DEFAULT_FALSE = __(
+ 'To define internal users, first enable new users set to external',
+);
function setUserInternalRegexPlaceholder(checkbox) {
- const userInternalRegex = document.getElementById('application_setting_user_default_internal_regex');
+ const userInternalRegex = document.getElementById(
+ 'application_setting_user_default_internal_regex',
+ );
if (checkbox && userInternalRegex) {
if (checkbox.checked) {
userInternalRegex.readOnly = false;
diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
index e7ceccb6f47..d5ded3f9a79 100644
--- a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
+++ b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
@@ -17,20 +17,24 @@ export default () => {
const previewPath = $('textarea#broadcast_message_message').data('previewPath');
- $('textarea#broadcast_message_message').on('input', _.debounce(function onMessageInput() {
- const message = $(this).val();
- if (message === '') {
- $('.js-broadcast-message-preview').text('Your message here');
- } else {
- axios.post(previewPath, {
- broadcast_message: {
- message,
- },
- })
- .then(({ data }) => {
- $('.js-broadcast-message-preview').html(data.message);
- })
- .catch(() => flash(__('An error occurred while rendering preview broadcast message')));
- }
- }, 250));
+ $('textarea#broadcast_message_message').on(
+ 'input',
+ _.debounce(function onMessageInput() {
+ const message = $(this).val();
+ if (message === '') {
+ $('.js-broadcast-message-preview').text('Your message here');
+ } else {
+ axios
+ .post(previewPath, {
+ broadcast_message: {
+ message,
+ },
+ })
+ .then(({ data }) => {
+ $('.js-broadcast-message-preview').html(data.message);
+ })
+ .catch(() => flash(__('An error occurred while rendering preview broadcast message')));
+ }
+ }, 250),
+ );
};
diff --git a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
index bc84666779e..e2fec3c7172 100644
--- a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
+++ b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
@@ -1,39 +1,42 @@
<script>
- import axios from '~/lib/utils/axios_utils';
- import createFlash from '~/flash';
- import GlModal from '~/vue_shared/components/gl_modal.vue';
- import { redirectTo } from '~/lib/utils/url_utility';
- import { s__ } from '~/locale';
+import axios from '~/lib/utils/axios_utils';
+import createFlash from '~/flash';
+import GlModal from '~/vue_shared/components/gl_modal.vue';
+import { redirectTo } from '~/lib/utils/url_utility';
+import { s__ } from '~/locale';
- export default {
- components: {
- GlModal,
+export default {
+ components: {
+ GlModal,
+ },
+ props: {
+ url: {
+ type: String,
+ required: true,
},
- props: {
- url: {
- type: String,
- required: true,
- },
+ },
+ computed: {
+ text() {
+ return s__(
+ 'AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running.',
+ );
},
- computed: {
- text() {
- return s__('AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running.');
- },
+ },
+ methods: {
+ onSubmit() {
+ return axios
+ .post(this.url)
+ .then(response => {
+ // follow the rediect to refresh the page
+ redirectTo(response.request.responseURL);
+ })
+ .catch(error => {
+ createFlash(s__('AdminArea|Stopping jobs failed'));
+ throw error;
+ });
},
- methods: {
- onSubmit() {
- return axios.post(this.url)
- .then((response) => {
- // follow the rediect to refresh the page
- redirectTo(response.request.responseURL);
- })
- .catch((error) => {
- createFlash(s__('AdminArea|Stopping jobs failed'));
- throw error;
- });
- },
- },
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/pages/admin/projects/index.js b/app/assets/javascripts/pages/admin/projects/index.js
index 31c96eb87af..d6b1e747aec 100644
--- a/app/assets/javascripts/pages/admin/projects/index.js
+++ b/app/assets/javascripts/pages/admin/projects/index.js
@@ -4,6 +4,7 @@ import NamespaceSelect from '../../../namespace_select';
document.addEventListener('DOMContentLoaded', () => {
new ProjectsList(); // eslint-disable-line no-new
- document.querySelectorAll('.js-namespace-select')
+ document
+ .querySelectorAll('.js-namespace-select')
.forEach(dropdown => new NamespaceSelect({ dropdown }));
});
diff --git a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
index ff66d3a8ac4..3c383735f4a 100644
--- a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
+++ b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
@@ -1,81 +1,84 @@
<script>
- import _ from 'underscore';
- import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
- import { s__, sprintf } from '~/locale';
+import _ from 'underscore';
+import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
+import { s__, sprintf } from '~/locale';
- export default {
- components: {
- DeprecatedModal,
+export default {
+ components: {
+ DeprecatedModal,
+ },
+ props: {
+ deleteProjectUrl: {
+ type: String,
+ required: false,
+ default: '',
},
- props: {
- deleteProjectUrl: {
- type: String,
- required: false,
- default: '',
- },
- projectName: {
- type: String,
- required: false,
- default: '',
- },
- csrfToken: {
- type: String,
- required: false,
- default: '',
- },
+ projectName: {
+ type: String,
+ required: false,
+ default: '',
},
- data() {
- return {
- enteredProjectName: '',
- };
+ csrfToken: {
+ type: String,
+ required: false,
+ default: '',
},
- computed: {
- title() {
- return sprintf(s__('AdminProjects|Delete Project %{projectName}?'),
- {
- projectName: `'${_.escape(this.projectName)}'`,
- },
- false,
- );
- },
- text() {
- return sprintf(s__(`AdminProjects|
+ },
+ data() {
+ return {
+ enteredProjectName: '',
+ };
+ },
+ computed: {
+ title() {
+ return sprintf(
+ s__('AdminProjects|Delete Project %{projectName}?'),
+ {
+ projectName: `'${_.escape(this.projectName)}'`,
+ },
+ false,
+ );
+ },
+ text() {
+ return sprintf(
+ s__(`AdminProjects|
You’re about to permanently delete the project %{projectName}, its repository,
and all related resources including issues, merge requests, etc.. Once you confirm and press
%{strong_start}Delete project%{strong_end}, it cannot be undone or recovered.`),
- {
- projectName: `<strong>${_.escape(this.projectName)}</strong>`,
- strong_start: '<strong>',
- strong_end: '</strong>',
- },
- false,
- );
- },
- confirmationTextLabel() {
- return sprintf(s__('AdminUsers|To confirm, type %{projectName}'),
- {
- projectName: `<code>${_.escape(this.projectName)}</code>`,
- },
- false,
- );
- },
- primaryButtonLabel() {
- return s__('AdminProjects|Delete project');
- },
- canSubmit() {
- return this.enteredProjectName === this.projectName;
- },
+ {
+ projectName: `<strong>${_.escape(this.projectName)}</strong>`,
+ strong_start: '<strong>',
+ strong_end: '</strong>',
+ },
+ false,
+ );
+ },
+ confirmationTextLabel() {
+ return sprintf(
+ s__('AdminUsers|To confirm, type %{projectName}'),
+ {
+ projectName: `<code>${_.escape(this.projectName)}</code>`,
+ },
+ false,
+ );
+ },
+ primaryButtonLabel() {
+ return s__('AdminProjects|Delete project');
+ },
+ canSubmit() {
+ return this.enteredProjectName === this.projectName;
+ },
+ },
+ methods: {
+ onCancel() {
+ this.enteredProjectName = '';
},
- methods: {
- onCancel() {
- this.enteredProjectName = '';
- },
- onSubmit() {
- this.$refs.form.submit();
- this.enteredProjectName = '';
- },
+ onSubmit() {
+ this.$refs.form.submit();
+ this.enteredProjectName = '';
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/pages/admin/projects/index/index.js b/app/assets/javascripts/pages/admin/projects/index/index.js
index ddbefec87b6..6fa8760545d 100644
--- a/app/assets/javascripts/pages/admin/projects/index/index.js
+++ b/app/assets/javascripts/pages/admin/projects/index/index.js
@@ -28,7 +28,7 @@ document.addEventListener('DOMContentLoaded', () => {
},
});
- $(document).on('shown.bs.modal', (event) => {
+ $(document).on('shown.bs.modal', event => {
if (event.relatedTarget.classList.contains('delete-project-button')) {
const buttonProps = event.relatedTarget.dataset;
deleteModal.deleteProjectUrl = buttonProps.deleteProjectUrl;
diff --git a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
index 8d5efcdcd96..4b33fcc759a 100644
--- a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
+++ b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
@@ -1,114 +1,119 @@
<script>
- import _ from 'underscore';
- import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
- import { s__, sprintf } from '~/locale';
+import _ from 'underscore';
+import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
+import { s__, sprintf } from '~/locale';
- export default {
- components: {
- DeprecatedModal,
+export default {
+ components: {
+ DeprecatedModal,
+ },
+ props: {
+ deleteUserUrl: {
+ type: String,
+ required: false,
+ default: '',
},
- props: {
- deleteUserUrl: {
- type: String,
- required: false,
- default: '',
- },
- blockUserUrl: {
- type: String,
- required: false,
- default: '',
- },
- deleteContributions: {
- type: Boolean,
- required: false,
- default: false,
- },
- username: {
- type: String,
- required: false,
- default: '',
- },
- csrfToken: {
- type: String,
- required: false,
- default: '',
- },
+ blockUserUrl: {
+ type: String,
+ required: false,
+ default: '',
},
- data() {
- return {
- enteredUsername: '',
- };
+ deleteContributions: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- computed: {
- title() {
- const keepContributionsTitle = s__('AdminUsers|Delete User %{username}?');
- const deleteContributionsTitle = s__('AdminUsers|Delete User %{username} and contributions?');
+ username: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ csrfToken: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ enteredUsername: '',
+ };
+ },
+ computed: {
+ title() {
+ const keepContributionsTitle = s__('AdminUsers|Delete User %{username}?');
+ const deleteContributionsTitle = s__('AdminUsers|Delete User %{username} and contributions?');
- return sprintf(
- this.deleteContributions ? deleteContributionsTitle : keepContributionsTitle, {
- username: `'${_.escape(this.username)}'`,
- }, false);
- },
- text() {
- const keepContributionsText = s__(`AdminArea|
+ return sprintf(
+ this.deleteContributions ? deleteContributionsTitle : keepContributionsTitle,
+ {
+ username: `'${_.escape(this.username)}'`,
+ },
+ false,
+ );
+ },
+ text() {
+ const keepContributionsText = s__(`AdminArea|
You are about to permanently delete the user %{username}.
Issues, merge requests, and groups linked to them will be transferred to a system-wide "Ghost-user".
To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead.
Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`);
- const deleteContributionsText = s__(`AdminArea|
+ const deleteContributionsText = s__(`AdminArea|
You are about to permanently delete the user %{username}.
This will delete all of the issues, merge requests, and groups linked to them.
To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead.
Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`);
- return sprintf(this.deleteContributions ? deleteContributionsText : keepContributionsText,
- {
- username: `<strong>${_.escape(this.username)}</strong>`,
- strong_start: '<strong>',
- strong_end: '</strong>',
- },
- false,
- );
- },
- confirmationTextLabel() {
- return sprintf(s__('AdminUsers|To confirm, type %{username}'),
- {
- username: `<code>${_.escape(this.username)}</code>`,
- },
- false,
- );
- },
- primaryButtonLabel() {
- const keepContributionsLabel = s__('AdminUsers|Delete user');
- const deleteContributionsLabel = s__('AdminUsers|Delete user and contributions');
+ return sprintf(
+ this.deleteContributions ? deleteContributionsText : keepContributionsText,
+ {
+ username: `<strong>${_.escape(this.username)}</strong>`,
+ strong_start: '<strong>',
+ strong_end: '</strong>',
+ },
+ false,
+ );
+ },
+ confirmationTextLabel() {
+ return sprintf(
+ s__('AdminUsers|To confirm, type %{username}'),
+ {
+ username: `<code>${_.escape(this.username)}</code>`,
+ },
+ false,
+ );
+ },
+ primaryButtonLabel() {
+ const keepContributionsLabel = s__('AdminUsers|Delete user');
+ const deleteContributionsLabel = s__('AdminUsers|Delete user and contributions');
- return this.deleteContributions ? deleteContributionsLabel : keepContributionsLabel;
- },
- secondaryButtonLabel() {
- return s__('AdminUsers|Block user');
- },
- canSubmit() {
- return this.enteredUsername === this.username;
- },
+ return this.deleteContributions ? deleteContributionsLabel : keepContributionsLabel;
},
- methods: {
- onCancel() {
- this.enteredUsername = '';
- },
- onSecondaryAction() {
- const { form } = this.$refs;
+ secondaryButtonLabel() {
+ return s__('AdminUsers|Block user');
+ },
+ canSubmit() {
+ return this.enteredUsername === this.username;
+ },
+ },
+ methods: {
+ onCancel() {
+ this.enteredUsername = '';
+ },
+ onSecondaryAction() {
+ const { form } = this.$refs;
- form.action = this.blockUserUrl;
- this.$refs.method.value = 'put';
+ form.action = this.blockUserUrl;
+ this.$refs.method.value = 'put';
- form.submit();
- },
- onSubmit() {
- this.$refs.form.submit();
- this.enteredUsername = '';
- },
+ form.submit();
+ },
+ onSubmit() {
+ this.$refs.form.submit();
+ this.enteredUsername = '';
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/pages/admin/users/index.js b/app/assets/javascripts/pages/admin/users/index.js
index 06599c3fd5f..45046688b57 100644
--- a/app/assets/javascripts/pages/admin/users/index.js
+++ b/app/assets/javascripts/pages/admin/users/index.js
@@ -32,12 +32,14 @@ document.addEventListener('DOMContentLoaded', () => {
},
});
- $(document).on('shown.bs.modal', (event) => {
+ $(document).on('shown.bs.modal', event => {
if (event.relatedTarget.classList.contains('delete-user-button')) {
const buttonProps = event.relatedTarget.dataset;
deleteModal.deleteUserUrl = buttonProps.deleteUserUrl;
deleteModal.blockUserUrl = buttonProps.blockUserUrl;
- deleteModal.deleteContributions = event.relatedTarget.hasAttribute('data-delete-contributions');
+ deleteModal.deleteContributions = event.relatedTarget.hasAttribute(
+ 'data-delete-contributions',
+ );
deleteModal.username = buttonProps.username;
}
});
diff --git a/app/assets/javascripts/pages/admin/users/new/index.js b/app/assets/javascripts/pages/admin/users/new/index.js
index 58bfa8d64e7..3e6a090cb0e 100644
--- a/app/assets/javascripts/pages/admin/users/new/index.js
+++ b/app/assets/javascripts/pages/admin/users/new/index.js
@@ -4,7 +4,9 @@ export default class UserInternalRegexHandler {
constructor() {
this.regexPattern = $('[data-user-internal-regex-pattern]').data('user-internal-regex-pattern');
if (this.regexPattern && this.regexPattern !== '') {
- this.regexOptions = $('[data-user-internal-regex-options]').data('user-internal-regex-options');
+ this.regexOptions = $('[data-user-internal-regex-options]').data(
+ 'user-internal-regex-options',
+ );
this.external = $('#user_external');
this.warningMessage = $('#warning_external_automatically_set');
this.addListenerToEmailField();
@@ -13,7 +15,7 @@ export default class UserInternalRegexHandler {
}
addListenerToEmailField() {
- $('#user_email').on('input', (event) => {
+ $('#user_email').on('input', event => {
this.setExternalCheckbox(event.currentTarget.value);
});
}
diff --git a/app/assets/javascripts/pages/dashboard/issues/index.js b/app/assets/javascripts/pages/dashboard/issues/index.js
index c4901dd1cb6..9055738f86e 100644
--- a/app/assets/javascripts/pages/dashboard/issues/index.js
+++ b/app/assets/javascripts/pages/dashboard/issues/index.js
@@ -1,7 +1,13 @@
import projectSelect from '~/project_select';
-import initLegacyFilters from '~/init_legacy_filters';
+import initFilteredSearch from '~/pages/search/init_filtered_search';
+import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
+import { FILTERED_SEARCH } from '~/pages/constants';
document.addEventListener('DOMContentLoaded', () => {
+ initFilteredSearch({
+ page: FILTERED_SEARCH.ISSUES,
+ filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
+ });
+
projectSelect();
- initLegacyFilters();
});
diff --git a/app/assets/javascripts/pages/dashboard/merge_requests/index.js b/app/assets/javascripts/pages/dashboard/merge_requests/index.js
index c4901dd1cb6..260484726f3 100644
--- a/app/assets/javascripts/pages/dashboard/merge_requests/index.js
+++ b/app/assets/javascripts/pages/dashboard/merge_requests/index.js
@@ -1,7 +1,15 @@
import projectSelect from '~/project_select';
-import initLegacyFilters from '~/init_legacy_filters';
+import initFilteredSearch from '~/pages/search/init_filtered_search';
+import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
+import { FILTERED_SEARCH } from '~/pages/constants';
document.addEventListener('DOMContentLoaded', () => {
+ IssuableFilteredSearchTokenKeys.addExtraTokensForMergeRequests();
+
+ initFilteredSearch({
+ page: FILTERED_SEARCH.MERGE_REQUESTS,
+ filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
+ });
+
projectSelect();
- initLegacyFilters();
});
diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
index 72f3f70b98f..1b56b97f751 100644
--- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js
+++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
@@ -79,7 +79,8 @@ export default class Todos {
.then(({ data }) => {
this.updateRowState(target);
this.updateBadges(data);
- }).catch(() => {
+ })
+ .catch(() => {
this.updateRowState(target, true);
return flash(__('Error updating todo status.'));
});
@@ -118,10 +119,12 @@ export default class Todos {
axios[target.dataset.method](target.dataset.href, {
ids: this.todo_ids,
- }).then(({ data }) => {
- this.updateAllState(target, data);
- this.updateBadges(data);
- }).catch(() => flash(__('Error updating status for all todos.')));
+ })
+ .then(({ data }) => {
+ this.updateAllState(target, data);
+ this.updateBadges(data);
+ })
+ .catch(() => flash(__('Error updating status for all todos.')));
}
updateAllState(target, data) {
@@ -133,7 +136,7 @@ export default class Todos {
target.removeAttribute('disabled');
target.classList.remove('disabled');
- this.todo_ids = (target === markAllDoneBtn) ? data.updated_ids : [];
+ this.todo_ids = target === markAllDoneBtn ? data.updated_ids : [];
undoAllBtn.classList.toggle('hidden');
markAllDoneBtn.classList.toggle('hidden');
todoListContainer.classList.toggle('hidden');
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/groups/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
index d3b2656743d..ae0a8c74964 100644
--- a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
@@ -9,7 +9,7 @@ document.addEventListener('DOMContentLoaded', () => {
// eslint-disable-next-line no-new
new AjaxVariableList({
container: variableListEl,
- saveButton: variableListEl.querySelector('.js-secret-variables-save-button'),
+ saveButton: variableListEl.querySelector('.js-ci-variables-save-button'),
errorBox: variableListEl.querySelector('.js-ci-variable-error-box'),
saveEndpoint: variableListEl.dataset.saveEndpoint,
});
diff --git a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
index 48668562f09..a4778077bc4 100644
--- a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
+++ b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
@@ -1,94 +1,117 @@
<script>
- import axios from '~/lib/utils/axios_utils';
+import axios from '~/lib/utils/axios_utils';
- import Flash from '~/flash';
- import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
- import { n__, s__, sprintf } from '~/locale';
- import { redirectTo } from '~/lib/utils/url_utility';
- import eventHub from '../event_hub';
+import Flash from '~/flash';
+import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
+import { n__, s__, sprintf } from '~/locale';
+import { redirectTo } from '~/lib/utils/url_utility';
+import eventHub from '../event_hub';
- export default {
- components: {
- DeprecatedModal,
+export default {
+ components: {
+ DeprecatedModal,
+ },
+ props: {
+ issueCount: {
+ type: Number,
+ required: true,
},
- props: {
- issueCount: {
- type: Number,
- required: true,
- },
- mergeRequestCount: {
- type: Number,
- required: true,
- },
- milestoneId: {
- type: Number,
- required: true,
- },
- milestoneTitle: {
- type: String,
- required: true,
- },
- milestoneUrl: {
- type: String,
- required: true,
- },
+ mergeRequestCount: {
+ type: Number,
+ required: true,
},
- computed: {
- text() {
- const milestoneTitle = sprintf('<strong>%{milestoneTitle}</strong>', { milestoneTitle: this.milestoneTitle });
-
- if (this.issueCount === 0 && this.mergeRequestCount === 0) {
- return sprintf(
- s__(`Milestones|
-You’re about to permanently delete the milestone %{milestoneTitle}.
-This milestone is not currently used in any issues or merge requests.`),
- {
- milestoneTitle,
- },
- false,
- );
- }
+ milestoneId: {
+ type: Number,
+ required: true,
+ },
+ milestoneTitle: {
+ type: String,
+ required: true,
+ },
+ milestoneUrl: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ text() {
+ const milestoneTitle = sprintf('<strong>%{milestoneTitle}</strong>', {
+ milestoneTitle: this.milestoneTitle,
+ });
+ if (this.issueCount === 0 && this.mergeRequestCount === 0) {
return sprintf(
s__(`Milestones|
-You’re about to permanently delete the milestone %{milestoneTitle} and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}.
-Once deleted, it cannot be undone or recovered.`),
+You’re about to permanently delete the milestone %{milestoneTitle}.
+This milestone is not currently used in any issues or merge requests.`),
{
milestoneTitle,
- issuesWithCount: n__('%d issue', '%d issues', this.issueCount),
- mergeRequestsWithCount: n__('%d merge request', '%d merge requests', this.mergeRequestCount),
},
false,
);
- },
- title() {
- return sprintf(s__('Milestones|Delete milestone %{milestoneTitle}?'), { milestoneTitle: this.milestoneTitle });
- },
- },
- methods: {
- onSubmit() {
- eventHub.$emit('deleteMilestoneModal.requestStarted', this.milestoneUrl);
+ }
- return axios.delete(this.milestoneUrl)
- .then((response) => {
- eventHub.$emit('deleteMilestoneModal.requestFinished', { milestoneUrl: this.milestoneUrl, successful: true });
+ return sprintf(
+ s__(`Milestones|
+You’re about to permanently delete the milestone %{milestoneTitle} and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}.
+Once deleted, it cannot be undone or recovered.`),
+ {
+ milestoneTitle,
+ issuesWithCount: n__('%d issue', '%d issues', this.issueCount),
+ mergeRequestsWithCount: n__(
+ '%d merge request',
+ '%d merge requests',
+ this.mergeRequestCount,
+ ),
+ },
+ false,
+ );
+ },
+ title() {
+ return sprintf(s__('Milestones|Delete milestone %{milestoneTitle}?'), {
+ milestoneTitle: this.milestoneTitle,
+ });
+ },
+ },
+ methods: {
+ onSubmit() {
+ eventHub.$emit('deleteMilestoneModal.requestStarted', this.milestoneUrl);
- // follow the rediect to milestones overview page
- redirectTo(response.request.responseURL);
- })
- .catch((error) => {
- eventHub.$emit('deleteMilestoneModal.requestFinished', { milestoneUrl: this.milestoneUrl, successful: false });
+ return axios
+ .delete(this.milestoneUrl)
+ .then(response => {
+ eventHub.$emit('deleteMilestoneModal.requestFinished', {
+ milestoneUrl: this.milestoneUrl,
+ successful: true,
+ });
- if (error.response && error.response.status === 404) {
- Flash(sprintf(s__('Milestones|Milestone %{milestoneTitle} was not found'), { milestoneTitle: this.milestoneTitle }));
- } else {
- Flash(sprintf(s__('Milestones|Failed to delete milestone %{milestoneTitle}'), { milestoneTitle: this.milestoneTitle }));
- }
- throw error;
+ // follow the rediect to milestones overview page
+ redirectTo(response.request.responseURL);
+ })
+ .catch(error => {
+ eventHub.$emit('deleteMilestoneModal.requestFinished', {
+ milestoneUrl: this.milestoneUrl,
+ successful: false,
});
- },
+
+ if (error.response && error.response.status === 404) {
+ Flash(
+ sprintf(s__('Milestones|Milestone %{milestoneTitle} was not found'), {
+ milestoneTitle: this.milestoneTitle,
+ }),
+ );
+ } else {
+ Flash(
+ sprintf(s__('Milestones|Failed to delete milestone %{milestoneTitle}'), {
+ milestoneTitle: this.milestoneTitle,
+ }),
+ );
+ }
+ throw error;
+ });
},
- };
+ },
+};
</script>
<template>
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/milestones/shared/delete_milestone_modal_init.js b/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js
index d51b5c221e3..1d559dc6e41 100644
--- a/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js
+++ b/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js
@@ -7,7 +7,9 @@ export default () => {
Vue.use(Translate);
const onRequestFinished = ({ milestoneUrl, successful }) => {
- const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`);
+ const button = document.querySelector(
+ `.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`,
+ );
if (!successful) {
button.removeAttribute('disabled');
@@ -16,14 +18,16 @@ export default () => {
button.querySelector('.js-loading-icon').classList.add('hidden');
};
- const onRequestStarted = (milestoneUrl) => {
- const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`);
+ const onRequestStarted = milestoneUrl => {
+ const button = document.querySelector(
+ `.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`,
+ );
button.setAttribute('disabled', '');
button.querySelector('.js-loading-icon').classList.remove('hidden');
eventHub.$once('deleteMilestoneModal.requestFinished', onRequestFinished);
};
- const onDeleteButtonClick = (event) => {
+ const onDeleteButtonClick = event => {
const button = event.currentTarget;
const modalProps = {
milestoneId: parseInt(button.dataset.milestoneId, 10),
@@ -37,12 +41,12 @@ export default () => {
};
const deleteMilestoneButtons = document.querySelectorAll('.js-delete-milestone-button');
- deleteMilestoneButtons.forEach((button) => {
+ deleteMilestoneButtons.forEach(button => {
button.addEventListener('click', onDeleteButtonClick);
});
eventHub.$once('deleteMilestoneModal.mounted', () => {
- deleteMilestoneButtons.forEach((button) => {
+ deleteMilestoneButtons.forEach(button => {
button.removeAttribute('disabled');
});
});
diff --git a/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js b/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js
index 8e79341e96a..fcc62a2b2af 100644
--- a/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js
+++ b/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js
@@ -7,20 +7,24 @@ Vue.use(Translate);
export default () => {
const onRequestFinished = ({ milestoneUrl, successful }) => {
- const button = document.querySelector(`.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`);
+ const button = document.querySelector(
+ `.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`,
+ );
if (!successful) {
button.removeAttribute('disabled');
}
};
- const onRequestStarted = (milestoneUrl) => {
- const button = document.querySelector(`.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`);
+ const onRequestStarted = milestoneUrl => {
+ const button = document.querySelector(
+ `.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`,
+ );
button.setAttribute('disabled', '');
eventHub.$once('promoteMilestoneModal.requestFinished', onRequestFinished);
};
- const onDeleteButtonClick = (event) => {
+ const onDeleteButtonClick = event => {
const button = event.currentTarget;
const modalProps = {
milestoneTitle: button.dataset.milestoneTitle,
@@ -32,12 +36,12 @@ export default () => {
};
const promoteMilestoneButtons = document.querySelectorAll('.js-promote-project-milestone-button');
- promoteMilestoneButtons.forEach((button) => {
+ promoteMilestoneButtons.forEach(button => {
button.addEventListener('click', onDeleteButtonClick);
});
eventHub.$once('promoteMilestoneModal.mounted', () => {
- promoteMilestoneButtons.forEach((button) => {
+ promoteMilestoneButtons.forEach(button => {
button.removeAttribute('disabled');
});
});
diff --git a/app/assets/javascripts/pages/profiles/index.js b/app/assets/javascripts/pages/profiles/index.js
index 04e50963699..883be18b336 100644
--- a/app/assets/javascripts/pages/profiles/index.js
+++ b/app/assets/javascripts/pages/profiles/index.js
@@ -3,9 +3,12 @@ import '~/profile/gl_crop';
import Profile from '~/profile/profile';
document.addEventListener('DOMContentLoaded', () => {
- $(document).on('input.ssh_key', '#key_key', function () { // eslint-disable-line func-names
+ // eslint-disable-next-line func-names
+ $(document).on('input.ssh_key', '#key_key', function() {
const $title = $('#key_title');
- const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/);
+ const comment = $(this)
+ .val()
+ .match(/^\S+ \S+ (.+)\n?$/);
// Extract the SSH Key title from its comment
if (comment && comment.length > 1) {
diff --git a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
index 8e8f47c21d8..417935e2ad0 100644
--- a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
+++ b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
@@ -5,7 +5,9 @@ document.addEventListener('DOMContentLoaded', () => {
const twoFactorNode = document.querySelector('.js-two-factor-auth');
const skippable = twoFactorNode.dataset.twoFactorSkippable === 'true';
if (skippable) {
- const button = `<a class="btn btn-sm btn-warning float-right" data-method="patch" href="${twoFactorNode.dataset.two_factor_skip_url}">Configure it later</a>`;
+ const button = `<a class="btn btn-sm btn-warning float-right" data-method="patch" href="${
+ twoFactorNode.dataset.two_factor_skip_url
+ }">Configure it later</a>`;
const flashAlert = document.querySelector('.flash-alert .container-fluid');
if (flashAlert) flashAlert.insertAdjacentHTML('beforeend', button);
}
diff --git a/app/assets/javascripts/pages/projects/branches/new/index.js b/app/assets/javascripts/pages/projects/branches/new/index.js
index a9658fd1eb4..13ff47d53c2 100644
--- a/app/assets/javascripts/pages/projects/branches/new/index.js
+++ b/app/assets/javascripts/pages/projects/branches/new/index.js
@@ -1,6 +1,11 @@
import $ from 'jquery';
import NewBranchForm from '~/new_branch_form';
-document.addEventListener('DOMContentLoaded', () => (
- new NewBranchForm($('.js-create-branch-form'), JSON.parse(document.getElementById('availableRefs').innerHTML))
-));
+document.addEventListener(
+ 'DOMContentLoaded',
+ () =>
+ new NewBranchForm(
+ $('.js-create-branch-form'),
+ JSON.parse(document.getElementById('availableRefs').innerHTML),
+ ),
+);
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/graphs/charts/index.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js
index 80159a82bd4..3ccad513c05 100644
--- a/app/assets/javascripts/pages/projects/graphs/charts/index.js
+++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js
@@ -31,14 +31,16 @@ document.addEventListener('DOMContentLoaded', () => {
const chartData = data => ({
labels: Object.keys(data),
- datasets: [{
- fillColor: 'rgba(220,220,220,0.5)',
- strokeColor: 'rgba(220,220,220,1)',
- barStrokeWidth: 1,
- barValueSpacing: 1,
- barDatasetSpacing: 1,
- data: _.values(data),
- }],
+ datasets: [
+ {
+ fillColor: 'rgba(220,220,220,0.5)',
+ strokeColor: 'rgba(220,220,220,1)',
+ barStrokeWidth: 1,
+ barValueSpacing: 1,
+ barDatasetSpacing: 1,
+ data: _.values(data),
+ },
+ ],
});
const hourData = chartData(projectChartData.hour);
@@ -51,7 +53,9 @@ document.addEventListener('DOMContentLoaded', () => {
responsiveChart($('#month-chart'), monthData);
const data = projectChartData.languages;
- const ctx = $('#languages-chart').get(0).getContext('2d');
+ const ctx = $('#languages-chart')
+ .get(0)
+ .getContext('2d');
const options = {
scaleOverlay: true,
responsive: true,
diff --git a/app/assets/javascripts/pages/projects/graphs/show/index.js b/app/assets/javascripts/pages/projects/graphs/show/index.js
index 71f629fbc13..f79c386b59e 100644
--- a/app/assets/javascripts/pages/projects/graphs/show/index.js
+++ b/app/assets/javascripts/pages/projects/graphs/show/index.js
@@ -7,7 +7,8 @@ import ContributorsStatGraph from './stat_graph_contributors';
document.addEventListener('DOMContentLoaded', () => {
const url = document.querySelector('.js-graphs-show').dataset.projectGraphPath;
- axios.get(url)
+ axios
+ .get(url)
.then(({ data }) => {
const graph = new ContributorsStatGraph();
graph.init(data);
diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js
index 58bb8c5b0c8..76613394af6 100644
--- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js
+++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js
@@ -3,7 +3,11 @@
import $ from 'jquery';
import _ from 'underscore';
import { n__, s__, createDateTimeFormat, sprintf } from '~/locale';
-import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph';
+import {
+ ContributorsGraph,
+ ContributorsAuthorGraph,
+ ContributorsMasterGraph,
+} from './stat_graph_contributors_graph';
import ContributorsStatGraphUtil from './stat_graph_contributors_util';
export default (function() {
@@ -14,7 +18,7 @@ export default (function() {
ContributorsStatGraph.prototype.init = function(log) {
var author_commits, total_commits;
this.parsed_log = ContributorsStatGraphUtil.parse_log(log);
- this.set_current_field("commits");
+ this.set_current_field('commits');
total_commits = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field);
author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field);
this.add_master_graph(total_commits);
@@ -31,23 +35,26 @@ export default (function() {
var limited_author_data;
this.authors = [];
limited_author_data = author_data.slice(0, 100);
- return _.each(limited_author_data, (function(_this) {
- return function(d) {
- var author_graph, author_header;
- author_header = _this.create_author_header(d);
- $(".contributors-list").append(author_header);
-
- author_graph = new ContributorsAuthorGraph(d.dates);
- _this.authors[d.author_name] = author_graph;
- return author_graph.draw();
- };
- })(this));
+ return _.each(
+ limited_author_data,
+ (function(_this) {
+ return function(d) {
+ var author_graph, author_header;
+ author_header = _this.create_author_header(d);
+ $('.contributors-list').append(author_header);
+
+ author_graph = new ContributorsAuthorGraph(d.dates);
+ _this.authors[d.author_name] = author_graph;
+ return author_graph.draw();
+ };
+ })(this),
+ );
};
ContributorsStatGraph.prototype.format_author_commit_info = function(author) {
var commits;
commits = $('<span/>', {
- "class": 'graph-author-commits-count'
+ class: 'graph-author-commits-count',
});
commits.text(n__('%d commit', '%d commits', author.commits));
return $('<span/>').append(commits);
@@ -56,13 +63,13 @@ export default (function() {
ContributorsStatGraph.prototype.create_author_header = function(author) {
var author_commit_info, author_commit_info_span, author_email, author_name, list_item;
list_item = $('<li/>', {
- "class": 'person',
- style: 'display: block;'
+ class: 'person',
+ style: 'display: block;',
});
author_name = $('<h4>' + author.author_name + '</h4>');
author_email = $('<p class="graph-author-email">' + author.author_email + '</p>');
author_commit_info_span = $('<span/>', {
- "class": 'commits'
+ class: 'commits',
});
author_commit_info = this.format_author_commit_info(author);
author_commit_info_span.html(author_commit_info);
@@ -80,37 +87,41 @@ export default (function() {
};
ContributorsStatGraph.prototype.redraw_authors = function() {
- $("ol").html("");
+ $('ol').html('');
const { x_domain } = ContributorsGraph.prototype;
- const author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain);
-
- return _.each(author_commits, (function(_this) {
- return function(d) {
- _this.redraw_author_commit_info(d);
- if (_this.authors[d.author_name] != null) {
- $(_this.authors[d.author_name].list_item).appendTo("ol");
- _this.authors[d.author_name].set_data(d.dates);
- return _this.authors[d.author_name].redraw();
- }
- return '';
- };
- })(this));
+ const author_commits = ContributorsStatGraphUtil.get_author_data(
+ this.parsed_log,
+ this.field,
+ x_domain,
+ );
+
+ return _.each(
+ author_commits,
+ (function(_this) {
+ return function(d) {
+ _this.redraw_author_commit_info(d);
+ if (_this.authors[d.author_name] != null) {
+ $(_this.authors[d.author_name].list_item).appendTo('ol');
+ _this.authors[d.author_name].set_data(d.dates);
+ return _this.authors[d.author_name].redraw();
+ }
+ return '';
+ };
+ })(this),
+ );
};
ContributorsStatGraph.prototype.set_current_field = function(field) {
- return this.field = field;
+ return (this.field = field);
};
ContributorsStatGraph.prototype.change_date_header = function() {
const { x_domain } = ContributorsGraph.prototype;
- const formattedDateRange = sprintf(
- s__('ContributorsPage|%{startDate} – %{endDate}'),
- {
- startDate: this.dateFormat.format(new Date(x_domain[0])),
- endDate: this.dateFormat.format(new Date(x_domain[1])),
- },
- );
+ const formattedDateRange = sprintf(s__('ContributorsPage|%{startDate} – %{endDate}'), {
+ startDate: this.dateFormat.format(new Date(x_domain[0])),
+ endDate: this.dateFormat.format(new Date(x_domain[1])),
+ });
return $('#date_header').text(formattedDateRange);
};
@@ -120,7 +131,7 @@ export default (function() {
if ($author != null) {
author_list_item = $(this.authors[author.author_name].list_item);
author_commit_info = this.format_author_commit_info(author);
- return author_list_item.find("span").html(author_commit_info);
+ return author_list_item.find('span').html(author_commit_info);
}
return '';
};
diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js
index 5f91686347a..377dce6c746 100644
--- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js
+++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js
@@ -11,10 +11,32 @@ import { brushX } from 'd3-brush';
import { timeParse } from 'd3-time-format';
import { dateTickFormat } from '~/lib/utils/tick_formats';
-const d3 = { extent, max, select, scaleTime, scaleLinear, axisLeft, axisBottom, area, brushX, timeParse };
+const d3 = {
+ extent,
+ max,
+ select,
+ scaleTime,
+ scaleLinear,
+ axisLeft,
+ axisBottom,
+ area,
+ brushX,
+ timeParse,
+};
const hasProp = {}.hasOwnProperty;
-const extend = function(child, parent) { for (const key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
+const extend = function(child, parent) {
+ for (const key in parent) {
+ if (hasProp.call(parent, key)) child[key] = parent[key];
+ }
+ function ctor() {
+ this.constructor = child;
+ }
+ ctor.prototype = parent.prototype;
+ child.prototype = new ctor();
+ child.__super__ = parent.prototype;
+ return child;
+};
export const ContributorsGraph = (function() {
function ContributorsGraph() {}
@@ -23,7 +45,7 @@ export const ContributorsGraph = (function() {
top: 20,
right: 10,
bottom: 30,
- left: 40
+ left: 40,
};
ContributorsGraph.prototype.x_domain = null;
@@ -33,35 +55,39 @@ export const ContributorsGraph = (function() {
ContributorsGraph.prototype.dates = [];
ContributorsGraph.prototype.determine_width = function(baseWidth, $parentElement) {
- const parentPaddingWidth = parseFloat($parentElement.css('padding-left')) + parseFloat($parentElement.css('padding-right'));
+ const parentPaddingWidth =
+ parseFloat($parentElement.css('padding-left')) +
+ parseFloat($parentElement.css('padding-right'));
const marginWidth = this.MARGIN.left + this.MARGIN.right;
return baseWidth - parentPaddingWidth - marginWidth;
};
ContributorsGraph.set_x_domain = function(data) {
- return ContributorsGraph.prototype.x_domain = data;
+ return (ContributorsGraph.prototype.x_domain = data);
};
ContributorsGraph.set_y_domain = function(data) {
- return ContributorsGraph.prototype.y_domain = [
- 0, d3.max(data, function(d) {
- return d.commits = d.commits || d.additions || d.deletions;
- })
- ];
+ return (ContributorsGraph.prototype.y_domain = [
+ 0,
+ d3.max(data, function(d) {
+ return (d.commits = d.commits || d.additions || d.deletions);
+ }),
+ ]);
};
ContributorsGraph.init_x_domain = function(data) {
- return ContributorsGraph.prototype.x_domain = d3.extent(data, function(d) {
+ return (ContributorsGraph.prototype.x_domain = d3.extent(data, function(d) {
return d.date;
- });
+ }));
};
ContributorsGraph.init_y_domain = function(data) {
- return ContributorsGraph.prototype.y_domain = [
- 0, d3.max(data, function(d) {
- return d.commits = d.commits || d.additions || d.deletions;
- })
- ];
+ return (ContributorsGraph.prototype.y_domain = [
+ 0,
+ d3.max(data, function(d) {
+ return (d.commits = d.commits || d.additions || d.deletions);
+ }),
+ ]);
};
ContributorsGraph.init_domain = function(data) {
@@ -70,7 +96,7 @@ export const ContributorsGraph = (function() {
};
ContributorsGraph.set_dates = function(data) {
- return ContributorsGraph.prototype.dates = data;
+ return (ContributorsGraph.prototype.dates = data);
};
ContributorsGraph.prototype.set_x_domain = function() {
@@ -87,20 +113,33 @@ export const ContributorsGraph = (function() {
};
ContributorsGraph.prototype.create_scale = function(width, height) {
- this.x = d3.scaleTime().range([0, width]).clamp(true);
- return this.y = d3.scaleLinear().range([height, 0]).nice();
+ this.x = d3
+ .scaleTime()
+ .range([0, width])
+ .clamp(true);
+ return (this.y = d3
+ .scaleLinear()
+ .range([height, 0])
+ .nice());
};
ContributorsGraph.prototype.draw_x_axis = function() {
- return this.svg.append("g").attr("class", "x axis").attr("transform", "translate(0, " + this.height + ")").call(this.x_axis);
+ return this.svg
+ .append('g')
+ .attr('class', 'x axis')
+ .attr('transform', 'translate(0, ' + this.height + ')')
+ .call(this.x_axis);
};
ContributorsGraph.prototype.draw_y_axis = function() {
- return this.svg.append("g").attr("class", "y axis").call(this.y_axis);
+ return this.svg
+ .append('g')
+ .attr('class', 'y axis')
+ .call(this.y_axis);
};
ContributorsGraph.prototype.set_data = function(data) {
- return this.data = data;
+ return (this.data = data);
};
return ContributorsGraph;
@@ -137,9 +176,9 @@ export const ContributorsMasterGraph = (function(superClass) {
};
ContributorsMasterGraph.prototype.parse_dates = function(data) {
- const parseDate = d3.timeParse("%Y-%m-%d");
+ const parseDate = d3.timeParse('%Y-%m-%d');
return data.forEach(function(d) {
- return d.date = parseDate(d.date);
+ return (d.date = parseDate(d.date));
});
};
@@ -148,42 +187,63 @@ export const ContributorsMasterGraph = (function(superClass) {
};
ContributorsMasterGraph.prototype.create_axes = function() {
- this.x_axis = d3.axisBottom()
+ this.x_axis = d3
+ .axisBottom()
.scale(this.x)
.tickFormat(dateTickFormat);
- return this.y_axis = d3.axisLeft().scale(this.y).ticks(5);
+ return (this.y_axis = d3
+ .axisLeft()
+ .scale(this.y)
+ .ticks(5));
};
ContributorsMasterGraph.prototype.create_svg = function() {
- this.svg = d3.select("#contributors-master")
- .append("svg")
- .attr("width", this.width + this.MARGIN.left + this.MARGIN.right)
- .attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom)
- .attr("class", "tint-box")
- .append("g")
- .attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
+ this.svg = d3
+ .select('#contributors-master')
+ .append('svg')
+ .attr('width', this.width + this.MARGIN.left + this.MARGIN.right)
+ .attr('height', this.height + this.MARGIN.top + this.MARGIN.bottom)
+ .attr('class', 'tint-box')
+ .append('g')
+ .attr('transform', 'translate(' + this.MARGIN.left + ',' + this.MARGIN.top + ')');
return this.svg;
};
ContributorsMasterGraph.prototype.create_area = function(x, y) {
- return this.area = d3.area().x(function(d) {
- return x(d.date);
- }).y0(this.height).y1(function(d) {
- d.commits = d.commits || d.additions || d.deletions;
- return y(d.commits);
- });
+ return (this.area = d3
+ .area()
+ .x(function(d) {
+ return x(d.date);
+ })
+ .y0(this.height)
+ .y1(function(d) {
+ d.commits = d.commits || d.additions || d.deletions;
+ return y(d.commits);
+ }));
};
ContributorsMasterGraph.prototype.create_brush = function() {
- return this.brush = d3.brushX(this.x).extent([[this.x.range()[0], 0], [this.x.range()[1], this.height]]).on("end", this.update_content);
+ return (this.brush = d3
+ .brushX(this.x)
+ .extent([[this.x.range()[0], 0], [this.x.range()[1], this.height]])
+ .on('end', this.update_content));
};
ContributorsMasterGraph.prototype.draw_path = function(data) {
- return this.svg.append("path").datum(data).attr("class", "area").attr("d", this.area);
+ return this.svg
+ .append('path')
+ .datum(data)
+ .attr('class', 'area')
+ .attr('d', this.area);
};
ContributorsMasterGraph.prototype.add_brush = function() {
- return this.svg.append("g").attr("class", "selection").call(this.brush).selectAll("rect").attr("height", this.height);
+ return this.svg
+ .append('g')
+ .attr('class', 'selection')
+ .call(this.brush)
+ .selectAll('rect')
+ .attr('height', this.height);
};
ContributorsMasterGraph.prototype.update_content = function() {
@@ -193,7 +253,7 @@ export const ContributorsMasterGraph = (function(superClass) {
} else {
ContributorsGraph.set_x_domain(this.x_max_domain);
}
- return $("#brush_change").trigger('change');
+ return $('#brush_change').trigger('change');
};
ContributorsMasterGraph.prototype.draw = function() {
@@ -216,9 +276,9 @@ export const ContributorsMasterGraph = (function(superClass) {
this.process_dates(this.data);
ContributorsGraph.set_y_domain(this.data);
this.set_y_domain();
- this.svg.select("path").datum(this.data);
- this.svg.select("path").attr("d", this.area);
- return this.svg.select(".y.axis").call(this.y_axis);
+ this.svg.select('path').datum(this.data);
+ this.svg.select('path').attr('d', this.area);
+ return this.svg.select('.y.axis').call(this.y_axis);
};
return ContributorsMasterGraph;
@@ -252,43 +312,58 @@ export const ContributorsAuthorGraph = (function(superClass) {
};
ContributorsAuthorGraph.prototype.create_axes = function() {
- this.x_axis = d3.axisBottom()
+ this.x_axis = d3
+ .axisBottom()
.scale(this.x)
.ticks(8)
.tickFormat(dateTickFormat);
- return this.y_axis = d3.axisLeft().scale(this.y).ticks(5);
+ return (this.y_axis = d3
+ .axisLeft()
+ .scale(this.y)
+ .ticks(5));
};
ContributorsAuthorGraph.prototype.create_area = function(x, y) {
- return this.area = d3.area().x(function(d) {
- const parseDate = d3.timeParse("%Y-%m-%d");
- return x(parseDate(d));
- }).y0(this.height).y1((function(_this) {
- return function(d) {
- if (_this.data[d] != null) {
- return y(_this.data[d]);
- } else {
- return y(0);
- }
- };
- })(this));
+ return (this.area = d3
+ .area()
+ .x(function(d) {
+ const parseDate = d3.timeParse('%Y-%m-%d');
+ return x(parseDate(d));
+ })
+ .y0(this.height)
+ .y1(
+ (function(_this) {
+ return function(d) {
+ if (_this.data[d] != null) {
+ return y(_this.data[d]);
+ } else {
+ return y(0);
+ }
+ };
+ })(this),
+ ));
};
ContributorsAuthorGraph.prototype.create_svg = function() {
const persons = document.querySelectorAll('.person');
this.list_item = persons[persons.length - 1];
- this.svg = d3.select(this.list_item)
- .append("svg")
- .attr("width", this.width + this.MARGIN.left + this.MARGIN.right)
- .attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom)
- .attr("class", "spark")
- .append("g")
- .attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
+ this.svg = d3
+ .select(this.list_item)
+ .append('svg')
+ .attr('width', this.width + this.MARGIN.left + this.MARGIN.right)
+ .attr('height', this.height + this.MARGIN.top + this.MARGIN.bottom)
+ .attr('class', 'spark')
+ .append('g')
+ .attr('transform', 'translate(' + this.MARGIN.left + ',' + this.MARGIN.top + ')');
return this.svg;
};
ContributorsAuthorGraph.prototype.draw_path = function(data) {
- return this.svg.append("path").datum(data).attr("class", "area-contributor").attr("d", this.area);
+ return this.svg
+ .append('path')
+ .datum(data)
+ .attr('class', 'area-contributor')
+ .attr('d', this.area);
};
ContributorsAuthorGraph.prototype.draw = function() {
@@ -304,10 +379,10 @@ export const ContributorsAuthorGraph = (function(superClass) {
ContributorsAuthorGraph.prototype.redraw = function() {
this.set_domain();
- this.svg.select("path").datum(this.dates);
- this.svg.select("path").attr("d", this.area);
- this.svg.select(".x.axis").call(this.x_axis);
- return this.svg.select(".y.axis").call(this.y_axis);
+ this.svg.select('path').datum(this.dates);
+ this.svg.select('path').attr('d', this.area);
+ this.svg.select('.x.axis').call(this.x_axis);
+ return this.svg.select('.y.axis').call(this.y_axis);
};
return ContributorsAuthorGraph;
diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js
index cd0e2bc023c..988ae164955 100644
--- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js
+++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js
@@ -26,12 +26,12 @@ export default {
by_author = _.toArray(by_author);
return {
total: total,
- by_author: by_author
+ by_author: by_author,
};
},
add_date: function(date, collection) {
collection[date] = {};
- return collection[date].date = date;
+ return (collection[date].date = date);
},
add_author: function(author, by_author, by_email) {
var data, normalized_email;
@@ -49,28 +49,28 @@ export default {
return this.store_deletions(entry, total, by_author);
},
store_commits: function(total, by_author) {
- this.add(total, "commits", 1);
- return this.add(by_author, "commits", 1);
+ this.add(total, 'commits', 1);
+ return this.add(by_author, 'commits', 1);
},
add: function(collection, field, value) {
if (collection[field] == null) {
collection[field] = 0;
}
- return collection[field] += value;
+ return (collection[field] += value);
},
store_additions: function(entry, total, by_author) {
if (entry.additions == null) {
entry.additions = 0;
}
- this.add(total, "additions", entry.additions);
- return this.add(by_author, "additions", entry.additions);
+ this.add(total, 'additions', entry.additions);
+ return this.add(by_author, 'additions', entry.additions);
},
store_deletions: function(entry, total, by_author) {
if (entry.deletions == null) {
entry.deletions = 0;
}
- this.add(total, "deletions", entry.deletions);
- return this.add(by_author, "deletions", entry.deletions);
+ this.add(total, 'deletions', entry.deletions);
+ return this.add(by_author, 'deletions', entry.deletions);
},
get_total_data: function(parsed_log, field) {
var log, total_data;
@@ -95,15 +95,18 @@ export default {
}
log = parsed_log.by_author;
author_data = [];
- _.each(log, (function(_this) {
- return function(log_entry) {
- var parsed_log_entry;
- parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range);
- if (!_.isEmpty(parsed_log_entry.dates)) {
- return author_data.push(parsed_log_entry);
- }
- };
- })(this));
+ _.each(
+ log,
+ (function(_this) {
+ return function(log_entry) {
+ var parsed_log_entry;
+ parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range);
+ if (!_.isEmpty(parsed_log_entry.dates)) {
+ return author_data.push(parsed_log_entry);
+ }
+ };
+ })(this),
+ );
return _.sortBy(author_data, function(d) {
return d[field];
}).reverse();
@@ -120,16 +123,19 @@ export default {
parsed_entry.additions = 0;
parsed_entry.deletions = 0;
- _.each(_.omit(log_entry, 'author_name', 'author_email'), (function(_this) {
- return function(value, key) {
- if (_this.in_range(value.date, date_range)) {
- parsed_entry.dates[value.date] = value[field];
- parsed_entry.commits += value.commits;
- parsed_entry.additions += value.additions;
- return parsed_entry.deletions += value.deletions;
- }
- };
- })(this));
+ _.each(
+ _.omit(log_entry, 'author_name', 'author_email'),
+ (function(_this) {
+ return function(value, key) {
+ if (_this.in_range(value.date, date_range)) {
+ parsed_entry.dates[value.date] = value[field];
+ parsed_entry.commits += value.commits;
+ parsed_entry.additions += value.additions;
+ return (parsed_entry.deletions += value.deletions);
+ }
+ };
+ })(this),
+ );
return parsed_entry;
},
in_range: function(date, date_range) {
@@ -139,5 +145,5 @@ export default {
} else {
return false;
}
- }
+ },
};
diff --git a/app/assets/javascripts/pages/projects/init_blob.js b/app/assets/javascripts/pages/projects/init_blob.js
index bc08ccf3584..bd8afa2d5ba 100644
--- a/app/assets/javascripts/pages/projects/init_blob.js
+++ b/app/assets/javascripts/pages/projects/init_blob.js
@@ -16,7 +16,8 @@ export default () => {
);
const fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url');
- const fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href');
+ const fileBlobPermalinkUrl =
+ fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href');
new ShortcutsNavigation(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/init_form.js b/app/assets/javascripts/pages/projects/init_form.js
index 9f20a3e4e46..019efe077f7 100644
--- a/app/assets/javascripts/pages/projects/init_form.js
+++ b/app/assets/javascripts/pages/projects/init_form.js
@@ -1,7 +1,7 @@
import ZenMode from '~/zen_mode';
import GLForm from '~/gl_form';
-export default function ($formEl) {
+export default function($formEl) {
new ZenMode(); // eslint-disable-line no-new
new GLForm($formEl); // eslint-disable-line no-new
}
diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js
index ef65196872c..8987c8e3f47 100644
--- a/app/assets/javascripts/pages/projects/issues/show.js
+++ b/app/assets/javascripts/pages/projects/issues/show.js
@@ -5,7 +5,7 @@ import ZenMode from '~/zen_mode';
import '~/notes/index';
import initIssueableApp from '~/issue_show';
-export default function () {
+export default function() {
initIssueableApp();
new Issue(); // eslint-disable-line no-new
new ShortcutsIssuable(); // eslint-disable-line no-new
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/labels/components/promote_label_modal.vue b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
index 5d2247f6c6d..e8b646f3f6e 100644
--- a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
+++ b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
@@ -1,72 +1,86 @@
<script>
- import _ from 'underscore';
- 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 _ from 'underscore';
+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: {
+ url: {
+ type: String,
+ required: true,
},
- props: {
- url: {
- type: String,
- required: true,
- },
- labelTitle: {
- type: String,
- required: true,
- },
- labelColor: {
- type: String,
- required: true,
- },
- labelTextColor: {
- type: String,
- required: true,
- },
- groupName: {
- type: String,
- required: true,
- },
+ labelTitle: {
+ type: String,
+ required: true,
},
- computed: {
- text() {
- return sprintf(s__(`Labels|Promoting %{labelTitle} will make it available for all projects inside %{groupName}.
- Existing project labels with the same title will be merged. This action cannot be reversed.`), {
+ labelColor: {
+ type: String,
+ required: true,
+ },
+ labelTextColor: {
+ type: String,
+ required: true,
+ },
+ groupName: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ text() {
+ return sprintf(
+ s__(`Labels|Promoting %{labelTitle} will make it available for all projects inside %{groupName}.
+ Existing project labels with the same title will be merged. This action cannot be reversed.`),
+ {
labelTitle: this.labelTitle,
groupName: this.groupName,
- });
- },
- title() {
- const label = `<span
+ },
+ );
+ },
+ title() {
+ const label = `<span
class="label color-label"
style="background-color: ${this.labelColor}; color: ${this.labelTextColor};"
>${_.escape(this.labelTitle)}</span>`;
- return sprintf(s__('Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>'), {
+ return sprintf(
+ s__('Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>'),
+ {
labelTitle: label,
- }, false);
- },
+ },
+ false,
+ );
},
- methods: {
- onSubmit() {
- eventHub.$emit('promoteLabelModal.requestStarted', this.url);
- return axios.post(this.url, { params: { format: 'json' } })
- .then((response) => {
- eventHub.$emit('promoteLabelModal.requestFinished', { labelUrl: this.url, successful: true });
- visitUrl(response.data.url);
- })
- .catch((error) => {
- eventHub.$emit('promoteLabelModal.requestFinished', { labelUrl: this.url, successful: false });
- createFlash(error);
+ },
+ methods: {
+ onSubmit() {
+ eventHub.$emit('promoteLabelModal.requestStarted', this.url);
+ return axios
+ .post(this.url, { params: { format: 'json' } })
+ .then(response => {
+ eventHub.$emit('promoteLabelModal.requestFinished', {
+ labelUrl: this.url,
+ successful: true,
+ });
+ visitUrl(response.data.url);
+ })
+ .catch(error => {
+ eventHub.$emit('promoteLabelModal.requestFinished', {
+ labelUrl: this.url,
+ successful: false,
});
- },
+ createFlash(error);
+ });
},
- };
+ },
+};
</script>
<template>
<gl-modal
diff --git a/app/assets/javascripts/pages/projects/labels/index/index.js b/app/assets/javascripts/pages/projects/labels/index/index.js
index 03cfef61311..36cf485f33d 100644
--- a/app/assets/javascripts/pages/projects/labels/index/index.js
+++ b/app/assets/javascripts/pages/projects/labels/index/index.js
@@ -10,20 +10,24 @@ const initLabelIndex = () => {
initLabels();
const onRequestFinished = ({ labelUrl, successful }) => {
- const button = document.querySelector(`.js-promote-project-label-button[data-url="${labelUrl}"]`);
+ const button = document.querySelector(
+ `.js-promote-project-label-button[data-url="${labelUrl}"]`,
+ );
if (!successful) {
button.removeAttribute('disabled');
}
};
- const onRequestStarted = (labelUrl) => {
- const button = document.querySelector(`.js-promote-project-label-button[data-url="${labelUrl}"]`);
+ const onRequestStarted = labelUrl => {
+ const button = document.querySelector(
+ `.js-promote-project-label-button[data-url="${labelUrl}"]`,
+ );
button.setAttribute('disabled', '');
eventHub.$once('promoteLabelModal.requestFinished', onRequestFinished);
};
- const onDeleteButtonClick = (event) => {
+ const onDeleteButtonClick = event => {
const button = event.currentTarget;
const modalProps = {
labelTitle: button.dataset.labelTitle,
@@ -37,12 +41,12 @@ const initLabelIndex = () => {
};
const promoteLabelButtons = document.querySelectorAll('.js-promote-project-label-button');
- promoteLabelButtons.forEach((button) => {
+ promoteLabelButtons.forEach(button => {
button.addEventListener('click', onDeleteButtonClick);
});
eventHub.$once('promoteLabelModal.mounted', () => {
- promoteLabelButtons.forEach((button) => {
+ promoteLabelButtons.forEach(button => {
button.removeAttribute('disabled');
});
});
diff --git a/app/assets/javascripts/pages/projects/network/network.js b/app/assets/javascripts/pages/projects/network/network.js
index 70fbb3f301c..226d63f05c4 100644
--- a/app/assets/javascripts/pages/projects/network/network.js
+++ b/app/assets/javascripts/pages/projects/network/network.js
@@ -6,13 +6,15 @@ import BranchGraph from '../../../network/branch_graph';
export default (function() {
function Network(opts) {
var vph;
- $("#filter_ref").click(function() {
- return $(this).closest('form').submit();
+ $('#filter_ref').click(function() {
+ return $(this)
+ .closest('form')
+ .submit();
});
- this.branch_graph = new BranchGraph($(".network-graph"), opts);
+ this.branch_graph = new BranchGraph($('.network-graph'), opts);
vph = $(window).height() - 250;
$('.network-graph').css({
- 'height': vph + 'px'
+ height: vph + 'px',
});
}
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js
index 544360dcd51..6197dc8a9db 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js
@@ -1,12 +1,16 @@
import Vue from 'vue';
import PipelineSchedulesCallout from '../shared/components/pipeline_schedules_callout.vue';
-document.addEventListener('DOMContentLoaded', () => new Vue({
- el: '#pipeline-schedules-callout',
- components: {
- 'pipeline-schedules-callout': PipelineSchedulesCallout,
- },
- render(createElement) {
- return createElement('pipeline-schedules-callout');
- },
-}));
+document.addEventListener(
+ 'DOMContentLoaded',
+ () =>
+ new Vue({
+ el: '#pipeline-schedules-callout',
+ components: {
+ 'pipeline-schedules-callout': PipelineSchedulesCallout,
+ },
+ render(createElement) {
+ return createElement('pipeline-schedules-callout');
+ },
+ }),
+);
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
index ef53d67e7cb..ab6f42d928c 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
@@ -1,63 +1,63 @@
<script>
- import _ from 'underscore';
+import _ from 'underscore';
- export default {
- props: {
- initialCronInterval: {
- type: String,
- required: false,
- default: '',
- },
- },
- data() {
- return {
- inputNameAttribute: 'schedule[cron]',
- cronInterval: this.initialCronInterval,
- cronIntervalPresets: {
- everyDay: '0 4 * * *',
- everyWeek: '0 4 * * 0',
- everyMonth: '0 4 1 * *',
- },
- cronSyntaxUrl: 'https://en.wikipedia.org/wiki/Cron',
- customInputEnabled: false,
- };
+export default {
+ props: {
+ initialCronInterval: {
+ type: String,
+ required: false,
+ default: '',
},
- computed: {
- intervalIsPreset() {
- return _.contains(this.cronIntervalPresets, this.cronInterval);
- },
- // The text input is editable when there's a custom interval, or when it's
- // a preset interval and the user clicks the 'custom' radio button
- isEditable() {
- return !!(this.customInputEnabled || !this.intervalIsPreset);
+ },
+ data() {
+ return {
+ inputNameAttribute: 'schedule[cron]',
+ cronInterval: this.initialCronInterval,
+ cronIntervalPresets: {
+ everyDay: '0 4 * * *',
+ everyWeek: '0 4 * * 0',
+ everyMonth: '0 4 1 * *',
},
+ cronSyntaxUrl: 'https://en.wikipedia.org/wiki/Cron',
+ customInputEnabled: false,
+ };
+ },
+ computed: {
+ intervalIsPreset() {
+ return _.contains(this.cronIntervalPresets, this.cronInterval);
},
- watch: {
- cronInterval() {
- // updates field validation state when model changes, as
- // glFieldError only updates on input.
- this.$nextTick(() => {
- gl.pipelineScheduleFieldErrors.updateFormValidityState();
- });
- },
+ // The text input is editable when there's a custom interval, or when it's
+ // a preset interval and the user clicks the 'custom' radio button
+ isEditable() {
+ return !!(this.customInputEnabled || !this.intervalIsPreset);
},
- created() {
- if (this.intervalIsPreset) {
- this.enableCustomInput = false;
- }
+ },
+ watch: {
+ cronInterval() {
+ // updates field validation state when model changes, as
+ // glFieldError only updates on input.
+ this.$nextTick(() => {
+ gl.pipelineScheduleFieldErrors.updateFormValidityState();
+ });
},
- methods: {
- toggleCustomInput(shouldEnable) {
- this.customInputEnabled = shouldEnable;
+ },
+ created() {
+ if (this.intervalIsPreset) {
+ this.enableCustomInput = false;
+ }
+ },
+ methods: {
+ toggleCustomInput(shouldEnable) {
+ this.customInputEnabled = shouldEnable;
- if (shouldEnable) {
- // We need to change the value so other radios don't remain selected
- // because the model (cronInterval) hasn't changed. The server trims it.
- this.cronInterval = `${this.cronInterval} `;
- }
- },
+ if (shouldEnable) {
+ // We need to change the value so other radios don't remain selected
+ // because the model (cronInterval) hasn't changed. The server trims it.
+ this.cronInterval = `${this.cronInterval} `;
+ }
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
index 77508e62cef..33fc2420e4d 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
@@ -1,31 +1,31 @@
<script>
- import Vue from 'vue';
- import Cookies from 'js-cookie';
- import Translate from '../../../../../vue_shared/translate';
- import illustrationSvg from '../icons/intro_illustration.svg';
+import Vue from 'vue';
+import Cookies from 'js-cookie';
+import Translate from '../../../../../vue_shared/translate';
+import illustrationSvg from '../icons/intro_illustration.svg';
- Vue.use(Translate);
+Vue.use(Translate);
- const cookieKey = 'pipeline_schedules_callout_dismissed';
+const cookieKey = 'pipeline_schedules_callout_dismissed';
- export default {
- name: 'PipelineSchedulesCallout',
- data() {
- return {
- docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl,
- calloutDismissed: Cookies.get(cookieKey) === 'true',
- };
+export default {
+ name: 'PipelineSchedulesCallout',
+ data() {
+ return {
+ docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl,
+ calloutDismissed: Cookies.get(cookieKey) === 'true',
+ };
+ },
+ created() {
+ this.illustrationSvg = illustrationSvg;
+ },
+ methods: {
+ dismissCallout() {
+ this.calloutDismissed = true;
+ Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 });
},
- created() {
- this.illustrationSvg = illustrationSvg;
- },
- methods: {
- dismissCallout() {
- this.calloutDismissed = true;
- Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 });
- },
- },
- };
+ },
+};
</script>
<template>
<div
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js
index 4ef0d11dd36..0057700c1b3 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js
@@ -26,8 +26,7 @@ export default class TargetBranchDropdown {
}
formatBranchesList() {
- return this.$dropdown.data('data')
- .map(val => ({ name: val }));
+ return this.$dropdown.data('data').map(val => ({ name: val }));
}
setDropdownToggle() {
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js
index c3ac54733a3..4d494efef6c 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js
@@ -11,7 +11,9 @@ Vue.use(Translate);
function initIntervalPatternInput() {
const intervalPatternMount = document.getElementById('interval-pattern-input');
- const initialCronInterval = intervalPatternMount ? intervalPatternMount.dataset.initialInterval : '';
+ const initialCronInterval = intervalPatternMount
+ ? intervalPatternMount.dataset.initialInterval
+ : '';
return new Vue({
el: intervalPatternMount,
diff --git a/app/assets/javascripts/pages/projects/pipelines/charts/index.js b/app/assets/javascripts/pages/projects/pipelines/charts/index.js
index 07b6992eba1..48353f3b4ef 100644
--- a/app/assets/javascripts/pages/projects/pipelines/charts/index.js
+++ b/app/assets/javascripts/pages/projects/pipelines/charts/index.js
@@ -7,26 +7,29 @@ const options = {
maintainAspectRatio: false,
};
-const buildChart = (chartScope) => {
+const buildChart = chartScope => {
const data = {
labels: chartScope.labels,
- datasets: [{
- fillColor: '#707070',
- strokeColor: '#707070',
- pointColor: '#707070',
- pointStrokeColor: '#EEE',
- data: chartScope.totalValues,
- },
- {
- fillColor: '#1aaa55',
- strokeColor: '#1aaa55',
- pointColor: '#1aaa55',
- pointStrokeColor: '#fff',
- data: chartScope.successValues,
- },
+ datasets: [
+ {
+ fillColor: '#707070',
+ strokeColor: '#707070',
+ pointColor: '#707070',
+ pointStrokeColor: '#EEE',
+ data: chartScope.totalValues,
+ },
+ {
+ fillColor: '#1aaa55',
+ strokeColor: '#1aaa55',
+ pointColor: '#1aaa55',
+ pointStrokeColor: '#fff',
+ data: chartScope.successValues,
+ },
],
};
- const ctx = $(`#${chartScope.scope}Chart`).get(0).getContext('2d');
+ const ctx = $(`#${chartScope.scope}Chart`)
+ .get(0)
+ .getContext('2d');
new Chart(ctx).Line(data, options);
};
@@ -36,14 +39,16 @@ document.addEventListener('DOMContentLoaded', () => {
const chartsData = JSON.parse(document.getElementById('pipelinesChartsData').innerHTML);
const data = {
labels: chartTimesData.labels,
- datasets: [{
- fillColor: 'rgba(220,220,220,0.5)',
- strokeColor: 'rgba(220,220,220,1)',
- barStrokeWidth: 1,
- barValueSpacing: 1,
- barDatasetSpacing: 1,
- data: chartTimesData.values,
- }],
+ datasets: [
+ {
+ fillColor: 'rgba(220,220,220,0.5)',
+ strokeColor: 'rgba(220,220,220,1)',
+ barStrokeWidth: 1,
+ barValueSpacing: 1,
+ barDatasetSpacing: 1,
+ data: chartTimesData.values,
+ },
+ ],
};
if (window.innerWidth < 768) {
@@ -51,7 +56,11 @@ document.addEventListener('DOMContentLoaded', () => {
options.scaleFontSize = 8;
}
- new Chart($('#build_timesChart').get(0).getContext('2d')).Bar(data, options);
+ new Chart(
+ $('#build_timesChart')
+ .get(0)
+ .getContext('2d'),
+ ).Bar(data, options);
chartsData.forEach(scope => buildChart(scope));
});
diff --git a/app/assets/javascripts/pages/projects/pipelines/index/index.js b/app/assets/javascripts/pages/projects/pipelines/index/index.js
index a84e2790680..fc337a7609b 100644
--- a/app/assets/javascripts/pages/projects/pipelines/index/index.js
+++ b/app/assets/javascripts/pages/projects/pipelines/index/index.js
@@ -6,35 +6,39 @@ import { convertPermissionToBoolean } from '../../../../lib/utils/common_utils';
Vue.use(Translate);
-document.addEventListener('DOMContentLoaded', () => new Vue({
- el: '#pipelines-list-vue',
- components: {
- pipelinesComponent,
- },
- data() {
- return {
- store: new PipelinesStore(),
- };
- },
- created() {
- this.dataset = document.querySelector(this.$options.el).dataset;
- },
- render(createElement) {
- return createElement('pipelines-component', {
- props: {
- store: this.store,
- endpoint: this.dataset.endpoint,
- helpPagePath: this.dataset.helpPagePath,
- emptyStateSvgPath: this.dataset.emptyStateSvgPath,
- errorStateSvgPath: this.dataset.errorStateSvgPath,
- noPipelinesSvgPath: this.dataset.noPipelinesSvgPath,
- autoDevopsPath: this.dataset.helpAutoDevopsPath,
- newPipelinePath: this.dataset.newPipelinePath,
- canCreatePipeline: convertPermissionToBoolean(this.dataset.canCreatePipeline),
- hasGitlabCi: convertPermissionToBoolean(this.dataset.hasGitlabCi),
- ciLintPath: this.dataset.ciLintPath,
- resetCachePath: this.dataset.resetCachePath,
+document.addEventListener(
+ 'DOMContentLoaded',
+ () =>
+ new Vue({
+ el: '#pipelines-list-vue',
+ components: {
+ pipelinesComponent,
},
- });
- },
-}));
+ data() {
+ return {
+ store: new PipelinesStore(),
+ };
+ },
+ created() {
+ this.dataset = document.querySelector(this.$options.el).dataset;
+ },
+ render(createElement) {
+ return createElement('pipelines-component', {
+ props: {
+ store: this.store,
+ endpoint: this.dataset.endpoint,
+ helpPagePath: this.dataset.helpPagePath,
+ emptyStateSvgPath: this.dataset.emptyStateSvgPath,
+ errorStateSvgPath: this.dataset.errorStateSvgPath,
+ noPipelinesSvgPath: this.dataset.noPipelinesSvgPath,
+ autoDevopsPath: this.dataset.helpAutoDevopsPath,
+ newPipelinePath: this.dataset.newPipelinePath,
+ canCreatePipeline: convertPermissionToBoolean(this.dataset.canCreatePipeline),
+ hasGitlabCi: convertPermissionToBoolean(this.dataset.hasGitlabCi),
+ ciLintPath: this.dataset.ciLintPath,
+ resetCachePath: this.dataset.resetCachePath,
+ },
+ });
+ },
+ }),
+);
diff --git a/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js b/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js
index 94dfeb96e8c..ba4ae04ab3d 100644
--- a/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js
+++ b/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js
@@ -2,9 +2,12 @@ import Pipelines from '~/pipelines';
export default () => {
const { controllerAction } = document.querySelector('.js-pipeline-container').dataset;
- const pipelineStatusUrl = `${document.querySelector('.js-pipeline-tab-link a').getAttribute('href')}/status.json`;
+ const pipelineStatusUrl = `${document
+ .querySelector('.js-pipeline-tab-link a')
+ .getAttribute('href')}/status.json`;
- new Pipelines({ // eslint-disable-line no-new
+ // eslint-disable-next-line no-new
+ new Pipelines({
initTabs: true,
pipelineStatusUrl,
tabsOptions: {
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/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
index 8f5ac3d8082..15c6fb550c1 100644
--- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
@@ -18,7 +18,7 @@ document.addEventListener('DOMContentLoaded', () => {
// eslint-disable-next-line no-new
new AjaxVariableList({
container: variableListEl,
- saveButton: variableListEl.querySelector('.js-secret-variables-save-button'),
+ saveButton: variableListEl.querySelector('.js-ci-variables-save-button'),
errorBox: variableListEl.querySelector('.js-ci-variable-error-box'),
saveEndpoint: variableListEl.dataset.saveEndpoint,
});
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue
index 06101290f6c..dced839c883 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue
@@ -1,73 +1,71 @@
<script>
- import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue';
+import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue';
- export default {
- components: {
- projectFeatureToggle,
- },
+export default {
+ components: {
+ projectFeatureToggle,
+ },
- model: {
- prop: 'value',
- event: 'change',
- },
+ model: {
+ prop: 'value',
+ event: 'change',
+ },
- props: {
- name: {
- type: String,
- required: false,
- default: '',
- },
- options: {
- type: Array,
- required: false,
- default: () => [],
- },
- value: {
- type: Number,
- required: false,
- default: 0,
- },
- disabledInput: {
- type: Boolean,
- required: false,
- default: false,
- },
+ props: {
+ name: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ options: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ value: {
+ type: Number,
+ required: false,
+ default: 0,
},
+ disabledInput: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
- computed: {
- featureEnabled() {
- return this.value !== 0;
- },
+ computed: {
+ featureEnabled() {
+ return this.value !== 0;
+ },
- displayOptions() {
- if (this.featureEnabled) {
- return this.options;
- }
- return [
- [0, 'Enable feature to choose access level'],
- ];
- },
+ displayOptions() {
+ if (this.featureEnabled) {
+ return this.options;
+ }
+ return [[0, 'Enable feature to choose access level']];
+ },
- displaySelectInput() {
- return this.disabledInput || !this.featureEnabled || this.displayOptions.length < 2;
- },
+ displaySelectInput() {
+ return this.disabledInput || !this.featureEnabled || this.displayOptions.length < 2;
},
+ },
- methods: {
- toggleFeature(featureEnabled) {
- if (featureEnabled === false || this.options.length < 1) {
- this.$emit('change', 0);
- } else {
- const [firstOptionValue] = this.options[this.options.length - 1];
- this.$emit('change', firstOptionValue);
- }
- },
+ methods: {
+ toggleFeature(featureEnabled) {
+ if (featureEnabled === false || this.options.length < 1) {
+ this.$emit('change', 0);
+ } else {
+ const [firstOptionValue] = this.options[this.options.length - 1];
+ this.$emit('change', firstOptionValue);
+ }
+ },
- selectOption(e) {
- this.$emit('change', Number(e.target.value));
- },
+ selectOption(e) {
+ this.$emit('change', Number(e.target.value));
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue
index 83437363af5..898d605463f 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue
@@ -1,23 +1,23 @@
<script>
- export default {
- props: {
- label: {
- type: String,
- required: false,
- default: null,
- },
- helpPath: {
- type: String,
- required: false,
- default: null,
- },
- helpText: {
- type: String,
- required: false,
- default: null,
- },
+export default {
+ props: {
+ label: {
+ type: String,
+ required: false,
+ default: null,
},
- };
+ helpPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ helpText: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+};
</script>
<template>
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/shared/permissions/constants.js b/app/assets/javascripts/pages/projects/shared/permissions/constants.js
index ce47562f259..bc5c29d12b5 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/constants.js
+++ b/app/assets/javascripts/pages/projects/shared/permissions/constants.js
@@ -5,7 +5,9 @@ export const visibilityOptions = {
};
export const visibilityLevelDescriptions = {
- [visibilityOptions.PRIVATE]: 'The project is accessible only by members of the project. Access must be granted explicitly to each user.',
+ [visibilityOptions.PRIVATE]:
+ 'The project is accessible only by members of the project. Access must be granted explicitly to each user.',
[visibilityOptions.INTERNAL]: 'The project can be accessed by any user who is logged in.',
- [visibilityOptions.PUBLIC]: 'The project can be accessed by anyone, regardless of authentication.',
+ [visibilityOptions.PUBLIC]:
+ 'The project can be accessed by anyone, regardless of authentication.',
};
diff --git a/app/assets/javascripts/pages/projects/shared/project_avatar.js b/app/assets/javascripts/pages/projects/shared/project_avatar.js
index 447877752fe..1e69ecb481d 100644
--- a/app/assets/javascripts/pages/projects/shared/project_avatar.js
+++ b/app/assets/javascripts/pages/projects/shared/project_avatar.js
@@ -8,8 +8,9 @@ export default function projectAvatar() {
$('.js-project-avatar-input').bind('change', function onClickAvatarInput() {
const form = $(this).closest('form');
- // eslint-disable-next-line no-useless-escape
- const filename = $(this).val().replace(/^.*[\\\/]/, '');
+ const filename = $(this)
+ .val()
+ .replace(/^.*[\\\/]/, ''); // eslint-disable-line no-useless-escape
return form.find('.js-avatar-filename').text(filename);
});
}
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..cbe85eaa590 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/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/pages/projects/wikis/index.js b/app/assets/javascripts/pages/projects/wikis/index.js
index c2629090f01..f5fd84d69ac 100644
--- a/app/assets/javascripts/pages/projects/wikis/index.js
+++ b/app/assets/javascripts/pages/projects/wikis/index.js
@@ -21,7 +21,8 @@ document.addEventListener('DOMContentLoaded', () => {
const { deleteWikiUrl, pageTitle } = deleteWikiModalWrapperEl.dataset;
- new Vue({ // eslint-disable-line no-new
+ // eslint-disable-next-line no-new
+ new Vue({
el: deleteWikiModalWrapperEl,
data: {
deleteWikiUrl: '',
diff --git a/app/assets/javascripts/pages/search/show/search.js b/app/assets/javascripts/pages/search/show/search.js
index e3e0ab91993..0c896c8599e 100644
--- a/app/assets/javascripts/pages/search/show/search.js
+++ b/app/assets/javascripts/pages/search/show/search.js
@@ -22,7 +22,7 @@ export default class Search {
fields: ['full_name'],
},
data(term, callback) {
- return Api.groups(term, {}, (data) => {
+ return Api.groups(term, {}, data => {
data.unshift({
full_name: 'Any',
});
@@ -37,7 +37,7 @@ export default class Search {
return obj.full_name;
},
toggleLabel(obj) {
- return `${($groupDropdown.data('defaultLabel'))} ${obj.full_name}`;
+ return `${$groupDropdown.data('defaultLabel')} ${obj.full_name}`;
},
clicked: () => Search.submitSearch(),
});
@@ -52,7 +52,7 @@ export default class Search {
},
data: (term, callback) => {
this.getProjectsData(term)
- .then((data) => {
+ .then(data => {
data.unshift({
name_with_namespace: 'Any',
});
@@ -70,7 +70,7 @@ export default class Search {
return obj.name_with_namespace;
},
toggleLabel(obj) {
- return `${($projectDropdown.data('defaultLabel'))} ${obj.name_with_namespace}`;
+ return `${$projectDropdown.data('defaultLabel')} ${obj.name_with_namespace}`;
},
clicked: () => Search.submitSearch(),
});
@@ -99,17 +99,24 @@ export default class Search {
}
clearSearchField() {
- return $(this.searchInput).val('').trigger('keyup').focus();
+ return $(this.searchInput)
+ .val('')
+ .trigger('keyup')
+ .focus();
}
getProjectsData(term) {
- return new Promise((resolve) => {
+ return new Promise(resolve => {
if (this.groupId) {
Api.groupProjects(this.groupId, term, {}, resolve);
} else {
- Api.projects(term, {
- order_by: 'id',
- }, resolve);
+ Api.projects(
+ term,
+ {
+ order_by: 'id',
+ },
+ resolve,
+ );
}
});
}
diff --git a/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js
index 1e7c29aefaa..2b8f1e8b0ef 100644
--- a/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js
+++ b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js
@@ -20,7 +20,7 @@ export default class SigninTabsMemoizer {
bootstrap() {
const tabs = document.querySelectorAll(this.tabSelector);
if (tabs.length > 0) {
- tabs[0].addEventListener('click', (e) => {
+ tabs[0].addEventListener('click', e => {
if (e.target && e.target.nodeName === 'A') {
const anchorName = e.target.getAttribute('href');
this.saveData(anchorName);
diff --git a/app/assets/javascripts/pages/sessions/new/username_validator.js b/app/assets/javascripts/pages/sessions/new/username_validator.js
index d621f988d86..7a41805bada 100644
--- a/app/assets/javascripts/pages/sessions/new/username_validator.js
+++ b/app/assets/javascripts/pages/sessions/new/username_validator.js
@@ -22,10 +22,10 @@ export default class UsernameValidator {
available: false,
valid: false,
pending: false,
- empty: true
+ empty: true,
};
- const debounceTimeout = _.debounce((username) => {
+ const debounceTimeout = _.debounce(username => {
this.validateUsername(username);
}, debounceTimeoutDuration);
@@ -81,7 +81,8 @@ export default class UsernameValidator {
this.state.pending = true;
this.state.available = false;
this.renderState();
- axios.get(`${gon.relative_url_root}/users/${username}/exists`)
+ axios
+ .get(`${gon.relative_url_root}/users/${username}/exists`)
.then(({ data }) => this.setAvailabilityState(data.exists))
.catch(() => flash(__('An error occurred while validating username')));
}
@@ -100,8 +101,7 @@ export default class UsernameValidator {
clearFieldValidationState() {
this.inputElement.siblings('p').hide();
- this.inputElement.removeClass(invalidInputClass)
- .removeClass(successInputClass);
+ this.inputElement.removeClass(invalidInputClass).removeClass(successInputClass);
}
setUnavailableState() {
diff --git a/app/assets/javascripts/pages/users/index.js b/app/assets/javascripts/pages/users/index.js
index 6b1626b0161..a191df00dfa 100644
--- a/app/assets/javascripts/pages/users/index.js
+++ b/app/assets/javascripts/pages/users/index.js
@@ -13,10 +13,12 @@ function initUserProfile(action) {
new UserTabs({ parentEl: '.user-profile', action });
// hide project limit message
- $('.hide-project-limit-message').on('click', (e) => {
+ $('.hide-project-limit-message').on('click', e => {
e.preventDefault();
Cookies.set('hide_project_limit_message', 'false');
- $(this).parents('.project-limit-message').remove();
+ $(this)
+ .parents('.project-limit-message')
+ .remove();
});
}
diff --git a/app/assets/javascripts/pages/users/user_overview_block.js b/app/assets/javascripts/pages/users/user_overview_block.js
index 2ed177be558..eec2b5ca8e5 100644
--- a/app/assets/javascripts/pages/users/user_overview_block.js
+++ b/app/assets/javascripts/pages/users/user_overview_block.js
@@ -10,6 +10,7 @@ export default class UserOverviewBlock {
limit: DEFAULT_LIMIT,
...options.requestParams,
};
+ this.postRenderCallback = options.postRenderCallback;
this.loadData();
}
@@ -43,5 +44,9 @@ export default class UserOverviewBlock {
}
loadingEl.classList.add('hide');
+
+ if (this.postRenderCallback) {
+ this.postRenderCallback.call(this);
+ }
}
}
diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js
index 04bcb16f036..aa537d4a43e 100644
--- a/app/assets/javascripts/pages/users/user_tabs.js
+++ b/app/assets/javascripts/pages/users/user_tabs.js
@@ -2,7 +2,8 @@ import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
import Activities from '~/activities';
import { localTimeAgo } from '~/lib/utils/datetime_utility';
-import { __, sprintf } from '~/locale';
+import AjaxCache from '~/lib/utils/ajax_cache';
+import { __ } from '~/locale';
import flash from '~/flash';
import ActivityCalendar from './activity_calendar';
import UserOverviewBlock from './user_overview_block';
@@ -62,23 +63,20 @@ import UserOverviewBlock from './user_overview_block';
* </div>
*/
-const CALENDAR_TEMPLATES = {
- activity: `
- <div class="clearfix calendar">
- <div class="js-contrib-calendar"></div>
- <div class="calendar-hint bottom-right"></div>
- </div>
- `,
- overview: `
- <div class="clearfix calendar">
- <div class="calendar-hint"></div>
- <div class="js-contrib-calendar prepend-top-20"></div>
- </div>
- `,
-};
+const CALENDAR_TEMPLATE = `
+ <div class="clearfix calendar">
+ <div class="js-contrib-calendar"></div>
+ <div class="calendar-hint bottom-right"></div>
+ </div>
+`;
const CALENDAR_PERIOD_6_MONTHS = 6;
const CALENDAR_PERIOD_12_MONTHS = 12;
+/* computation based on
+ * width = (group + 1) * this.daySizeWithSpace + this.getExtraWidthPadding(group);
+ * (see activity_calendar.js)
+ */
+const OVERVIEW_CALENDAR_BREAKPOINT = 918;
export default class UserTabs {
constructor({ defaultAction, action, parentEl }) {
@@ -105,6 +103,12 @@ export default class UserTabs {
.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]')
.on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event))
.on('click', '.gl-pagination a', event => this.changeProjectsPage(event));
+
+ window.addEventListener('resize', () => this.onResize());
+ }
+
+ onResize() {
+ this.loadActivityCalendar();
}
changeProjectsPage(e) {
@@ -167,8 +171,6 @@ export default class UserTabs {
return;
}
- this.loadActivityCalendar('activity');
-
// eslint-disable-next-line no-new
new Activities('#activity');
@@ -180,10 +182,10 @@ export default class UserTabs {
return;
}
- this.loadActivityCalendar('overview');
+ this.loadActivityCalendar();
UserTabs.renderMostRecentBlocks('#js-overview .activities-block', {
- requestParams: { limit: 5 },
+ requestParams: { limit: 10 },
});
UserTabs.renderMostRecentBlocks('#js-overview .projects-block', {
requestParams: { limit: 10, skip_pagination: true },
@@ -198,52 +200,39 @@ export default class UserTabs {
container,
url: $(`${container} .overview-content-list`).data('href'),
...options,
+ postRenderCallback: () => localTimeAgo($('.js-timeago', container)),
});
}
- loadActivityCalendar(action) {
- const monthsAgo = action === 'overview' ? CALENDAR_PERIOD_6_MONTHS : CALENDAR_PERIOD_12_MONTHS;
+ loadActivityCalendar() {
const $calendarWrap = this.$parentEl.find('.tab-pane.active .user-calendar');
const calendarPath = $calendarWrap.data('calendarPath');
+
+ AjaxCache.retrieve(calendarPath)
+ .then(data => UserTabs.renderActivityCalendar(data, $calendarWrap))
+ .catch(() => flash(__('There was an error loading users activity calendar.')));
+ }
+
+ static renderActivityCalendar(data, $calendarWrap) {
+ const monthsAgo = UserTabs.getVisibleCalendarPeriod($calendarWrap);
const calendarActivitiesPath = $calendarWrap.data('calendarActivitiesPath');
const utcOffset = $calendarWrap.data('utcOffset');
- let utcFormatted = 'UTC';
- if (utcOffset !== 0) {
- utcFormatted = `UTC${utcOffset > 0 ? '+' : ''}${utcOffset / 3600}`;
- }
+ const calendarHint = __('Issues, merge requests, pushes and comments.');
- axios
- .get(calendarPath)
- .then(({ data }) => {
- $calendarWrap.html(CALENDAR_TEMPLATES[action]);
-
- let calendarHint = '';
-
- if (action === 'activity') {
- calendarHint = sprintf(
- __(
- 'Summary of issues, merge requests, push events, and comments (Timezone: %{utcFormatted})',
- ),
- { utcFormatted },
- );
- } else if (action === 'overview') {
- calendarHint = __('Issues, merge requests, pushes and comments.');
- }
-
- $calendarWrap.find('.calendar-hint').text(calendarHint);
-
- // eslint-disable-next-line no-new
- new ActivityCalendar(
- '.tab-pane.active .js-contrib-calendar',
- '.tab-pane.active .user-calendar-activities',
- data,
- calendarActivitiesPath,
- utcOffset,
- 0,
- monthsAgo,
- );
- })
- .catch(() => flash(__('There was an error loading users activity calendar.')));
+ $calendarWrap.html(CALENDAR_TEMPLATE);
+
+ $calendarWrap.find('.calendar-hint').text(calendarHint);
+
+ // eslint-disable-next-line no-new
+ new ActivityCalendar(
+ '.tab-pane.active .js-contrib-calendar',
+ '.tab-pane.active .user-calendar-activities',
+ data,
+ calendarActivitiesPath,
+ utcOffset,
+ 0,
+ monthsAgo,
+ );
}
toggleLoading(status) {
@@ -267,4 +256,11 @@ export default class UserTabs {
getCurrentAction() {
return this.$parentEl.find('.nav-links a.active').data('action');
}
+
+ static getVisibleCalendarPeriod($calendarWrap) {
+ const width = $calendarWrap.width();
+ return width < OVERVIEW_CALENDAR_BREAKPOINT
+ ? CALENDAR_PERIOD_6_MONTHS
+ : CALENDAR_PERIOD_12_MONTHS;
+ }
}
diff --git a/app/assets/javascripts/performance_bar/components/simple_metric.vue b/app/assets/javascripts/performance_bar/components/simple_metric.vue
index 760ea8fe1e6..7a558558c4d 100644
--- a/app/assets/javascripts/performance_bar/components/simple_metric.vue
+++ b/app/assets/javascripts/performance_bar/components/simple_metric.vue
@@ -1,29 +1,29 @@
<script>
- export default {
- props: {
- currentRequest: {
- type: Object,
- required: true,
- },
- metric: {
- type: String,
- required: true,
- },
+export default {
+ props: {
+ currentRequest: {
+ type: Object,
+ required: true,
},
- computed: {
- duration() {
- return (
- this.currentRequest.details[this.metric] &&
- this.currentRequest.details[this.metric].duration
- );
- },
- calls() {
- return (
- this.currentRequest.details[this.metric] && this.currentRequest.details[this.metric].calls
- );
- },
+ metric: {
+ type: String,
+ required: true,
},
- };
+ },
+ computed: {
+ duration() {
+ return (
+ this.currentRequest.details[this.metric] &&
+ this.currentRequest.details[this.metric].duration
+ );
+ },
+ calls() {
+ return (
+ this.currentRequest.details[this.metric] && this.currentRequest.details[this.metric].calls
+ );
+ },
+ },
+};
</script>
<template>
<div
diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js
index 6e5ef0ac0b2..29bfb7ee5df 100644
--- a/app/assets/javascripts/performance_bar/index.js
+++ b/app/assets/javascripts/performance_bar/index.js
@@ -9,8 +9,7 @@ export default ({ container }) =>
performanceBarApp: () => import('./components/performance_bar_app.vue'),
},
data() {
- const performanceBarData = document.querySelector(this.$options.el)
- .dataset;
+ const performanceBarData = document.querySelector(this.$options.el).dataset;
const store = new PerformanceBarStore();
return {
diff --git a/app/assets/javascripts/performance_bar/services/performance_bar_service.js b/app/assets/javascripts/performance_bar/services/performance_bar_service.js
index 60d9ba62570..3a496fa2ed8 100644
--- a/app/assets/javascripts/performance_bar/services/performance_bar_service.js
+++ b/app/assets/javascripts/performance_bar/services/performance_bar_service.js
@@ -11,8 +11,10 @@ export default class PerformanceBarService {
static registerInterceptor(peekUrl, callback) {
const interceptor = response => {
- const [fireCallback, requestId, requestUrl] =
- PerformanceBarService.callbackParams(response, peekUrl);
+ const [fireCallback, requestId, requestUrl] = PerformanceBarService.callbackParams(
+ response,
+ peekUrl,
+ );
if (fireCallback) {
callback(requestId, requestUrl);
@@ -30,10 +32,7 @@ export default class PerformanceBarService {
static removeInterceptor(interceptor) {
axios.interceptors.response.eject(interceptor);
- Vue.http.interceptors = _.without(
- Vue.http.interceptors,
- vueResourceInterceptor,
- );
+ Vue.http.interceptors = _.without(Vue.http.interceptors, vueResourceInterceptor);
}
static callbackParams(response, peekUrl) {
diff --git a/app/assets/javascripts/performance_bar/stores/performance_bar_store.js b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js
index c6b2f55243c..031e774d533 100644
--- a/app/assets/javascripts/performance_bar/stores/performance_bar_store.js
+++ b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js
@@ -32,8 +32,6 @@ export default class PerformanceBarStore {
}
canTrackRequest(requestUrl) {
- return (
- this.requests.filter(request => request.url === requestUrl).length < 2
- );
+ return this.requests.filter(request => request.url === requestUrl).length < 2;
}
}
diff --git a/app/assets/javascripts/pipelines/components/empty_state.vue b/app/assets/javascripts/pipelines/components/empty_state.vue
index c5a45afc634..f756c651684 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/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..3a39dfe181d 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/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..6c9a11c3829 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/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..2670ea29db6 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/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..e6abf32decc 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/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..ac7fa4b195e 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/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..5104fe36b42 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/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..249f7b9f368 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/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 16e69759091..112c1559f25 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
@@ -1,16 +1,19 @@
<script>
+import { GlButton, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
-import { formatTime } from '~/lib/utils/datetime_utility';
+import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
import eventHub from '../event_hub';
-import icon from '../../vue_shared/components/icon.vue';
-import tooltip from '../../vue_shared/directives/tooltip';
+import Icon from '../../vue_shared/components/icon.vue';
export default {
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
components: {
- icon,
+ Icon,
+ GlCountdown,
+ GlButton,
+ GlLoadingIcon,
},
props: {
actions: {
@@ -28,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 },
);
@@ -51,24 +54,17 @@ export default {
return !action.playable;
},
-
- remainingTime(action) {
- const remainingMilliseconds = new Date(action.scheduled_at).getTime() - Date.now();
- return formatTime(Math.max(0, remainingMilliseconds));
- },
},
};
</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
@@ -80,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)"
>
@@ -100,9 +95,9 @@ export default {
class="pull-right"
>
<icon name="clock" />
- {{ remainingTime(action) }}
+ <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..d7d9eb00faa 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/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..1d62472671a 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/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..32bfa47e5f2 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/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/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
index 974629fa2af..99b57f4c9d5 100644
--- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue
+++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
@@ -1,78 +1,78 @@
<script>
- import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
- import { __, s__, sprintf } from '~/locale';
- import csrf from '~/lib/utils/csrf';
+import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
+import { __, s__, sprintf } from '~/locale';
+import csrf from '~/lib/utils/csrf';
- export default {
- components: {
- DeprecatedModal,
+export default {
+ components: {
+ DeprecatedModal,
+ },
+ props: {
+ actionUrl: {
+ type: String,
+ required: true,
},
- props: {
- actionUrl: {
- type: String,
- required: true,
- },
- confirmWithPassword: {
- type: Boolean,
- required: true,
- },
- username: {
- type: String,
- required: true,
- },
+ confirmWithPassword: {
+ type: Boolean,
+ required: true,
},
- data() {
- return {
- enteredPassword: '',
- enteredUsername: '',
- };
+ username: {
+ type: String,
+ required: true,
},
- computed: {
- csrfToken() {
- return csrf.token;
- },
- inputLabel() {
- let confirmationValue;
- if (this.confirmWithPassword) {
- confirmationValue = __('password');
- } else {
- confirmationValue = __('username');
- }
+ },
+ data() {
+ return {
+ enteredPassword: '',
+ enteredUsername: '',
+ };
+ },
+ computed: {
+ csrfToken() {
+ return csrf.token;
+ },
+ inputLabel() {
+ let confirmationValue;
+ if (this.confirmWithPassword) {
+ confirmationValue = __('password');
+ } else {
+ confirmationValue = __('username');
+ }
- confirmationValue = `<code>${confirmationValue}</code>`;
+ confirmationValue = `<code>${confirmationValue}</code>`;
- return sprintf(
- s__('Profiles|Type your %{confirmationValue} to confirm:'),
- { confirmationValue },
- false,
- );
- },
- text() {
- return sprintf(
- s__(`Profiles|
+ return sprintf(
+ s__('Profiles|Type your %{confirmationValue} to confirm:'),
+ { confirmationValue },
+ false,
+ );
+ },
+ text() {
+ return sprintf(
+ s__(`Profiles|
You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account.
Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
- {
- yourAccount: `<strong>${s__('Profiles|your account')}</strong>`,
- deleteAccount: `<strong>${s__('Profiles|Delete Account')}</strong>`,
- },
- false,
- );
- },
+ {
+ yourAccount: `<strong>${s__('Profiles|your account')}</strong>`,
+ deleteAccount: `<strong>${s__('Profiles|Delete Account')}</strong>`,
+ },
+ false,
+ );
},
- methods: {
- canSubmit() {
- if (this.confirmWithPassword) {
- return this.enteredPassword !== '';
- }
+ },
+ methods: {
+ canSubmit() {
+ if (this.confirmWithPassword) {
+ return this.enteredPassword !== '';
+ }
- return this.enteredUsername === this.username;
- },
- onSubmit() {
- this.$refs.form.submit();
- },
+ return this.enteredUsername === this.username;
+ },
+ onSubmit() {
+ this.$refs.form.submit();
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js
index af134881f31..befe91c332f 100644
--- a/app/assets/javascripts/profile/gl_crop.js
+++ b/app/assets/javascripts/profile/gl_crop.js
@@ -4,20 +4,35 @@ import $ from 'jquery';
import 'cropper';
import _ from 'underscore';
-((global) => {
+(global => {
// Matches everything but the file name
const FILENAMEREGEX = /^.*[\\\/]/;
class GitLabCrop {
- constructor(input, { filename, previewImage, modalCrop, pickImageEl, uploadImageBtn, modalCropImg,
- exportWidth = 200, exportHeight = 200, cropBoxWidth = 200, cropBoxHeight = 200 } = {}) {
+ constructor(
+ input,
+ {
+ filename,
+ previewImage,
+ modalCrop,
+ pickImageEl,
+ uploadImageBtn,
+ modalCropImg,
+ exportWidth = 200,
+ exportHeight = 200,
+ cropBoxWidth = 200,
+ cropBoxHeight = 200,
+ } = {},
+ ) {
this.onUploadImageBtnClick = this.onUploadImageBtnClick.bind(this);
this.onModalHide = this.onModalHide.bind(this);
this.onModalShow = this.onModalShow.bind(this);
this.onPickImageClick = this.onPickImageClick.bind(this);
this.fileInput = $(input);
this.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg;
- this.fileInput.attr('name', `${this.fileInput.attr('name')}-trigger`).attr('id', `${this.fileInput.attr('id')}-trigger`);
+ this.fileInput
+ .attr('name', `${this.fileInput.attr('name')}-trigger`)
+ .attr('id', `${this.fileInput.attr('id')}-trigger`);
this.exportWidth = exportWidth;
this.exportHeight = exportHeight;
this.cropBoxWidth = cropBoxWidth;
@@ -59,7 +74,7 @@ import _ from 'underscore';
btn = this;
return _this.onActionBtnClick(btn);
});
- return this.croppedImageBlob = null;
+ return (this.croppedImageBlob = null);
}
onPickImageClick() {
@@ -94,9 +109,9 @@ import _ from 'underscore';
width: cropBoxWidth,
height: cropBoxHeight,
left: (container.width - cropBoxWidth) / 2,
- top: (container.height - cropBoxHeight) / 2
+ top: (container.height - cropBoxHeight) / 2,
});
- }
+ },
});
}
@@ -116,7 +131,7 @@ import _ from 'underscore';
var data, result;
data = $(btn).data();
if (this.modalCropImg.data('cropper') && data.method) {
- return result = this.modalCropImg.cropper(data.method, data.option);
+ return (result = this.modalCropImg.cropper(data.method, data.option));
}
}
@@ -127,7 +142,7 @@ import _ from 'underscore';
readFile(input) {
var _this, reader;
_this = this;
- reader = new FileReader;
+ reader = new FileReader();
reader.onload = () => {
_this.modalCropImg.attr('src', reader.result);
return _this.modalCrop.modal('show');
@@ -145,7 +160,7 @@ import _ from 'underscore';
array.push(binary.charCodeAt(i));
}
return new Blob([new Uint8Array(array)], {
- type: 'image/png'
+ type: 'image/png',
});
}
@@ -157,11 +172,13 @@ import _ from 'underscore';
}
setBlob() {
- this.dataURL = this.modalCropImg.cropper('getCroppedCanvas', {
- width: 200,
- height: 200
- }).toDataURL('image/png');
- return this.croppedImageBlob = this.dataURLtoBlob(this.dataURL);
+ this.dataURL = this.modalCropImg
+ .cropper('getCroppedCanvas', {
+ width: 200,
+ height: 200,
+ })
+ .toDataURL('image/png');
+ return (this.croppedImageBlob = this.dataURLtoBlob(this.dataURL));
}
getBlob() {
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index e49c67ffb5c..8704a655b28 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -26,11 +26,7 @@ export default class Profile {
}
bindEvents() {
- $('.js-preferences-form').on(
- 'change.preference',
- 'input[type=radio]',
- this.submitForm,
- );
+ $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
$('#user_notification_email').on('change', this.submitForm);
$('#user_notified_of_own_activity').on('change', this.submitForm);
this.form.on('submit', this.onSubmitForm);
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..5a3407693e5 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/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..7400b685c7e 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/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/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js
index b601b19e7be..48343c8ba0a 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_create.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js
@@ -46,8 +46,12 @@ export default class ProtectedBranchCreate {
onSelect() {
// Enable submit button
const $branchInput = this.$form.find('input[name="protected_branch[name]"]');
- const $allowedToMergeInput = this.$form.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]');
- const $allowedToPushInput = this.$form.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]');
+ const $allowedToMergeInput = this.$form.find(
+ 'input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]',
+ );
+ const $allowedToPushInput = this.$form.find(
+ 'input[name="protected_branch[push_access_levels_attributes][0][access_level]"]',
+ );
const completedForm = !(
$branchInput.val() &&
$allowedToMergeInput.length &&
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js
index 54560d08ad7..5bc08f60d16 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js
@@ -29,8 +29,12 @@ export default class ProtectedBranchEdit {
}
onSelect() {
- const $allowedToMergeInput = this.$wrap.find(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`);
- const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`);
+ const $allowedToMergeInput = this.$wrap.find(
+ `input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`,
+ );
+ const $allowedToPushInput = this.$wrap.find(
+ `input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`,
+ );
// Do not update if one dropdown has not selected any option
if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return;
@@ -38,25 +42,36 @@ export default class ProtectedBranchEdit {
this.$allowedToMergeDropdown.disable();
this.$allowedToPushDropdown.disable();
- axios.patch(this.$wrap.data('url'), {
- protected_branch: {
- merge_access_levels_attributes: [{
- id: this.$allowedToMergeDropdown.data('accessLevelId'),
- access_level: $allowedToMergeInput.val(),
- }],
- push_access_levels_attributes: [{
- id: this.$allowedToPushDropdown.data('accessLevelId'),
- access_level: $allowedToPushInput.val(),
- }],
- },
- }).then(() => {
- this.$allowedToMergeDropdown.enable();
- this.$allowedToPushDropdown.enable();
- }).catch(() => {
- this.$allowedToMergeDropdown.enable();
- this.$allowedToPushDropdown.enable();
-
- flash('Failed to update branch!', 'alert', document.querySelector('.js-protected-branches-list'));
- });
+ axios
+ .patch(this.$wrap.data('url'), {
+ protected_branch: {
+ merge_access_levels_attributes: [
+ {
+ id: this.$allowedToMergeDropdown.data('accessLevelId'),
+ access_level: $allowedToMergeInput.val(),
+ },
+ ],
+ push_access_levels_attributes: [
+ {
+ id: this.$allowedToPushDropdown.data('accessLevelId'),
+ access_level: $allowedToPushInput.val(),
+ },
+ ],
+ },
+ })
+ .then(() => {
+ this.$allowedToMergeDropdown.enable();
+ this.$allowedToPushDropdown.enable();
+ })
+ .catch(() => {
+ this.$allowedToMergeDropdown.enable();
+ this.$allowedToPushDropdown.enable();
+
+ flash(
+ 'Failed to update branch!',
+ 'alert',
+ document.querySelector('.js-protected-branches-list'),
+ );
+ });
}
}
diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js
index 2f8116df0d2..fddf2674cbb 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_create.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_create.js
@@ -40,7 +40,9 @@ export default class ProtectedTagCreate {
const $tagInput = this.$form.find('input[name="protected_tag[name]"]');
const $allowedToCreateInput = this.$form.find('#create_access_levels_attributes');
- this.$form.find('input[type="submit"]').prop('disabled', !($tagInput.val() && $allowedToCreateInput.length));
+ this.$form
+ .find('input[type="submit"]')
+ .prop('disabled', !($tagInput.val() && $allowedToCreateInput.length));
}
static getProtectedTags(term, callback) {
diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js
index 8687b2a4044..c52497e62f2 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_edit.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_edit.js
@@ -21,26 +21,33 @@ export default class ProtectedTagEdit {
}
onSelect() {
- const $allowedToCreateInput = this.$wrap.find(`input[name="${this.$allowedToCreateDropdownButton.data('fieldName')}"]`);
+ const $allowedToCreateInput = this.$wrap.find(
+ `input[name="${this.$allowedToCreateDropdownButton.data('fieldName')}"]`,
+ );
// Do not update if one dropdown has not selected any option
if (!$allowedToCreateInput.length) return;
this.$allowedToCreateDropdownButton.disable();
- axios.patch(this.$wrap.data('url'), {
- protected_tag: {
- create_access_levels_attributes: [{
- id: this.$allowedToCreateDropdownButton.data('accessLevelId'),
- access_level: $allowedToCreateInput.val(),
- }],
- },
- }).then(() => {
- this.$allowedToCreateDropdownButton.enable();
- }).catch(() => {
- this.$allowedToCreateDropdownButton.enable();
-
- flash('Failed to update tag!', 'alert', document.querySelector('.js-protected-tags-list'));
- });
+ axios
+ .patch(this.$wrap.data('url'), {
+ protected_tag: {
+ create_access_levels_attributes: [
+ {
+ id: this.$allowedToCreateDropdownButton.data('accessLevelId'),
+ access_level: $allowedToCreateInput.val(),
+ },
+ ],
+ },
+ })
+ .then(() => {
+ this.$allowedToCreateDropdownButton.enable();
+ })
+ .catch(() => {
+ this.$allowedToCreateDropdownButton.enable();
+
+ flash('Failed to update tag!', 'alert', document.querySelector('.js-protected-tags-list'));
+ });
}
}
diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue
index 7e2287ac4db..6f94f5d6d2a 100644
--- a/app/assets/javascripts/registry/components/app.vue
+++ b/app/assets/javascripts/registry/components/app.vue
@@ -1,42 +1,37 @@
<script>
- import { mapGetters, mapActions } from 'vuex';
- import Flash from '../../flash';
- import store from '../stores';
- import collapsibleContainer from './collapsible_container.vue';
- import { errorMessages, errorMessagesTypes } from '../constants';
+import { mapGetters, mapActions } from 'vuex';
+import { GlLoadingIcon } from '@gitlab/ui';
+import Flash from '../../flash';
+import store from '../stores';
+import collapsibleContainer from './collapsible_container.vue';
+import { errorMessages, errorMessagesTypes } from '../constants';
- export default {
- name: 'RegistryListApp',
- components: {
- collapsibleContainer,
+export default {
+ name: 'RegistryListApp',
+ components: {
+ collapsibleContainer,
+ GlLoadingIcon,
+ },
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
},
- props: {
- endpoint: {
- type: String,
- required: true,
- },
- },
- store,
- computed: {
- ...mapGetters([
- 'isLoading',
- 'repos',
- ]),
- },
- created() {
- this.setMainEndpoint(this.endpoint);
- },
- mounted() {
- this.fetchRepos()
- .catch(() => Flash(errorMessages[errorMessagesTypes.FETCH_REPOS]));
- },
- methods: {
- ...mapActions([
- 'setMainEndpoint',
- 'fetchRepos',
- ]),
- },
- };
+ },
+ store,
+ computed: {
+ ...mapGetters(['isLoading', 'repos']),
+ },
+ created() {
+ this.setMainEndpoint(this.endpoint);
+ },
+ mounted() {
+ this.fetchRepos().catch(() => Flash(errorMessages[errorMessagesTypes.FETCH_REPOS]));
+ },
+ methods: {
+ ...mapActions(['setMainEndpoint', 'fetchRepos']),
+ },
+};
</script>
<template>
<div>
diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue
index d9bf41924d1..d85de973740 100644
--- a/app/assets/javascripts/registry/components/collapsible_container.vue
+++ b/app/assets/javascripts/registry/components/collapsible_container.vue
@@ -1,62 +1,61 @@
<script>
- import { mapActions } from 'vuex';
- import Flash from '../../flash';
- import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
- import tooltip from '../../vue_shared/directives/tooltip';
- import tableRegistry from './table_registry.vue';
- import { errorMessages, errorMessagesTypes } from '../constants';
- import { __ } from '../../locale';
+import { mapActions } from 'vuex';
+import { GlLoadingIcon } from '@gitlab/ui';
+import Flash from '../../flash';
+import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
+import tooltip from '../../vue_shared/directives/tooltip';
+import tableRegistry from './table_registry.vue';
+import { errorMessages, errorMessagesTypes } from '../constants';
+import { __ } from '../../locale';
- export default {
- name: 'CollapsibeContainerRegisty',
- components: {
- clipboardButton,
- tableRegistry,
+export default {
+ name: 'CollapsibeContainerRegisty',
+ components: {
+ clipboardButton,
+ tableRegistry,
+ GlLoadingIcon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ repo: {
+ type: Object,
+ required: true,
},
- directives: {
- tooltip,
- },
- props: {
- repo: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- isOpen: false,
- };
- },
- methods: {
- ...mapActions([
- 'fetchRepos',
- 'fetchList',
- 'deleteRepo',
- ]),
+ },
+ data() {
+ return {
+ isOpen: false,
+ };
+ },
+ methods: {
+ ...mapActions(['fetchRepos', 'fetchList', 'deleteRepo']),
- toggleRepo() {
- this.isOpen = !this.isOpen;
+ toggleRepo() {
+ this.isOpen = !this.isOpen;
- if (this.isOpen) {
- this.fetchList({ repo: this.repo })
- .catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY));
- }
- },
+ if (this.isOpen) {
+ this.fetchList({ repo: this.repo }).catch(() =>
+ this.showError(errorMessagesTypes.FETCH_REGISTRY),
+ );
+ }
+ },
- handleDeleteRepository() {
- this.deleteRepo(this.repo)
- .then(() => {
- Flash(__('This container registry has been scheduled for deletion.'), 'notice');
- this.fetchRepos();
- })
- .catch(() => this.showError(errorMessagesTypes.DELETE_REPO));
- },
+ handleDeleteRepository() {
+ this.deleteRepo(this.repo)
+ .then(() => {
+ Flash(__('This container registry has been scheduled for deletion.'), 'notice');
+ this.fetchRepos();
+ })
+ .catch(() => this.showError(errorMessagesTypes.DELETE_REPO));
+ },
- showError(message) {
- Flash(errorMessages[message]);
- },
+ showError(message) {
+ Flash(errorMessages[message]);
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue
index fafb35bd69a..bb6c977fc63 100644
--- a/app/assets/javascripts/registry/components/table_registry.vue
+++ b/app/assets/javascripts/registry/components/table_registry.vue
@@ -1,66 +1,62 @@
<script>
- import { mapActions } from 'vuex';
- import { n__ } from '../../locale';
- import Flash from '../../flash';
- import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
- import tablePagination from '../../vue_shared/components/table_pagination.vue';
- import tooltip from '../../vue_shared/directives/tooltip';
- import timeagoMixin from '../../vue_shared/mixins/timeago';
- import { errorMessages, errorMessagesTypes } from '../constants';
- import { numberToHumanSize } from '../../lib/utils/number_utils';
+import { mapActions } from 'vuex';
+import { n__ } from '../../locale';
+import Flash from '../../flash';
+import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
+import tablePagination from '../../vue_shared/components/table_pagination.vue';
+import tooltip from '../../vue_shared/directives/tooltip';
+import timeagoMixin from '../../vue_shared/mixins/timeago';
+import { errorMessages, errorMessagesTypes } from '../constants';
+import { numberToHumanSize } from '../../lib/utils/number_utils';
- export default {
- components: {
- clipboardButton,
- tablePagination,
+export default {
+ components: {
+ clipboardButton,
+ tablePagination,
+ },
+ directives: {
+ tooltip,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ repo: {
+ type: Object,
+ required: true,
},
- directives: {
- tooltip,
+ },
+ computed: {
+ shouldRenderPagination() {
+ return this.repo.pagination.total > this.repo.pagination.perPage;
},
- mixins: [
- timeagoMixin,
- ],
- props: {
- repo: {
- type: Object,
- required: true,
- },
- },
- computed: {
- shouldRenderPagination() {
- return this.repo.pagination.total > this.repo.pagination.perPage;
- },
- },
- methods: {
- ...mapActions([
- 'fetchList',
- 'deleteRegistry',
- ]),
+ },
+ methods: {
+ ...mapActions(['fetchList', 'deleteRegistry']),
- layers(item) {
- return item.layers ? n__('%d layer', '%d layers', item.layers) : '';
- },
+ layers(item) {
+ return item.layers ? n__('%d layer', '%d layers', item.layers) : '';
+ },
- formatSize(size) {
- return numberToHumanSize(size);
- },
+ formatSize(size) {
+ return numberToHumanSize(size);
+ },
- handleDeleteRegistry(registry) {
- this.deleteRegistry(registry)
- .then(() => this.fetchList({ repo: this.repo }))
- .catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY));
- },
+ handleDeleteRegistry(registry) {
+ this.deleteRegistry(registry)
+ .then(() => this.fetchList({ repo: this.repo }))
+ .catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY));
+ },
- onPageChange(pageNumber) {
- this.fetchList({ repo: this.repo, page: pageNumber })
- .catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY));
- },
+ onPageChange(pageNumber) {
+ this.fetchList({ repo: this.repo, page: pageNumber }).catch(() =>
+ this.showError(errorMessagesTypes.FETCH_REGISTRY),
+ );
+ },
- showError(message) {
- Flash(errorMessages[message]);
- },
+ showError(message) {
+ Flash(errorMessages[message]);
},
- };
+ },
+};
</script>
<template>
<div>
diff --git a/app/assets/javascripts/registry/index.js b/app/assets/javascripts/registry/index.js
index e15cd94a915..025afefe7f0 100644
--- a/app/assets/javascripts/registry/index.js
+++ b/app/assets/javascripts/registry/index.js
@@ -4,22 +4,23 @@ import Translate from '../vue_shared/translate';
Vue.use(Translate);
-export default () => new Vue({
- el: '#js-vue-registry-images',
- components: {
- registryApp,
- },
- data() {
- const { dataset } = document.querySelector(this.$options.el);
- return {
- endpoint: dataset.endpoint,
- };
- },
- render(createElement) {
- return createElement('registry-app', {
- props: {
- endpoint: this.endpoint,
- },
- });
- },
-});
+export default () =>
+ new Vue({
+ el: '#js-vue-registry-images',
+ components: {
+ registryApp,
+ },
+ data() {
+ const { dataset } = document.querySelector(this.$options.el);
+ return {
+ endpoint: dataset.endpoint,
+ };
+ },
+ render(createElement) {
+ return createElement('registry-app', {
+ props: {
+ endpoint: this.endpoint,
+ },
+ });
+ },
+ });
diff --git a/app/assets/javascripts/registry/stores/mutations.js b/app/assets/javascripts/registry/stores/mutations.js
index 208c3c39866..69c051cd2d6 100644
--- a/app/assets/javascripts/registry/stores/mutations.js
+++ b/app/assets/javascripts/registry/stores/mutations.js
@@ -2,7 +2,6 @@ import * as types from './mutation_types';
import { parseIntPagination, normalizeHeaders } from '../../lib/utils/common_utils';
export default {
-
[types.SET_MAIN_ENDPOINT](state, endpoint) {
Object.assign(state, { endpoint });
},
diff --git a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
index b373d83a44b..bd204503cc7 100644
--- a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
+++ b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
@@ -1,79 +1,72 @@
<script>
- import { mapActions, mapGetters, mapState } from 'vuex';
- import { s__ } from '~/locale';
- import { componentNames } from './issue_body';
- import ReportSection from './report_section.vue';
- import SummaryRow from './summary_row.vue';
- import IssuesList from './issues_list.vue';
- import Modal from './modal.vue';
- import createStore from '../store';
- import { summaryTextBuilder, reportTextBuilder, statusIcon } from '../store/utils';
+import { mapActions, mapGetters, mapState } from 'vuex';
+import { s__ } from '~/locale';
+import { componentNames } from './issue_body';
+import ReportSection from './report_section.vue';
+import SummaryRow from './summary_row.vue';
+import IssuesList from './issues_list.vue';
+import Modal from './modal.vue';
+import createStore from '../store';
+import { summaryTextBuilder, reportTextBuilder, statusIcon } from '../store/utils';
- export default {
- name: 'GroupedTestReportsApp',
- store: createStore(),
- components: {
- ReportSection,
- SummaryRow,
- IssuesList,
- Modal,
+export default {
+ name: 'GroupedTestReportsApp',
+ store: createStore(),
+ components: {
+ ReportSection,
+ SummaryRow,
+ IssuesList,
+ Modal,
+ },
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
},
- props: {
- endpoint: {
- type: String,
- required: true,
- },
- },
- componentNames,
- computed: {
- ...mapState([
- 'reports',
- 'isLoading',
- 'hasError',
- 'summary',
- ]),
- ...mapState({
- modalTitle: state => state.modal.title || '',
- modalData: state => state.modal.data || {},
- }),
- ...mapGetters([
- 'summaryStatus',
- ]),
- groupedSummaryText() {
- if (this.isLoading) {
- return s__('Reports|Test summary results are being parsed');
- }
+ },
+ componentNames,
+ computed: {
+ ...mapState(['reports', 'isLoading', 'hasError', 'summary']),
+ ...mapState({
+ modalTitle: state => state.modal.title || '',
+ modalData: state => state.modal.data || {},
+ }),
+ ...mapGetters(['summaryStatus']),
+ groupedSummaryText() {
+ if (this.isLoading) {
+ return s__('Reports|Test summary results are being parsed');
+ }
- if (this.hasError) {
- return s__('Reports|Test summary failed loading results');
- }
+ if (this.hasError) {
+ return s__('Reports|Test summary failed loading results');
+ }
- return summaryTextBuilder(s__('Reports|Test summary'), this.summary);
- },
+ return summaryTextBuilder(s__('Reports|Test summary'), this.summary);
},
- created() {
- this.setEndpoint(this.endpoint);
+ },
+ created() {
+ this.setEndpoint(this.endpoint);
- this.fetchReports();
+ this.fetchReports();
+ },
+ methods: {
+ ...mapActions(['setEndpoint', 'fetchReports']),
+ reportText(report) {
+ const summary = report.summary || {};
+ return reportTextBuilder(report.name, summary);
+ },
+ getReportIcon(report) {
+ return statusIcon(report.status);
},
- methods: {
- ...mapActions(['setEndpoint', 'fetchReports']),
- reportText(report) {
- const summary = report.summary || {};
- return reportTextBuilder(report.name, summary);
- },
- getReportIcon(report) {
- return statusIcon(report.status);
- },
- shouldRenderIssuesList(report) {
- return (
- report.existing_failures.length > 0 ||
- report.new_failures.length > 0 ||
- report.resolved_failures.length > 0
- );
- },
+ shouldRenderIssuesList(report) {
+ return (
+ report.existing_failures.length > 0 ||
+ report.new_failures.length > 0 ||
+ report.resolved_failures.length > 0
+ );
},
- };
+ },
+};
</script>
<template>
<report-section
diff --git a/app/assets/javascripts/reports/components/issue_status_icon.vue b/app/assets/javascripts/reports/components/issue_status_icon.vue
index 85811698a37..6e143c4f98c 100644
--- a/app/assets/javascripts/reports/components/issue_status_icon.vue
+++ b/app/assets/javascripts/reports/components/issue_status_icon.vue
@@ -1,10 +1,6 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
-import {
- STATUS_FAILED,
- STATUS_NEUTRAL,
- STATUS_SUCCESS,
-} from '../constants';
+import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '../constants';
export default {
name: 'IssueStatusIcon',
diff --git a/app/assets/javascripts/reports/components/issues_list.vue b/app/assets/javascripts/reports/components/issues_list.vue
index df42201b5de..f4243522ef8 100644
--- a/app/assets/javascripts/reports/components/issues_list.vue
+++ b/app/assets/javascripts/reports/components/issues_list.vue
@@ -1,22 +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,
@@ -44,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/modal.vue b/app/assets/javascripts/reports/components/modal.vue
index acc5c6d85e2..5f9e4072b2d 100644
--- a/app/assets/javascripts/reports/components/modal.vue
+++ b/app/assets/javascripts/reports/components/modal.vue
@@ -1,27 +1,27 @@
<script>
- import Modal from '~/vue_shared/components/gl_modal.vue';
- import LoadingButton from '~/vue_shared/components/loading_button.vue';
- import CodeBlock from '~/vue_shared/components/code_block.vue';
- import { fieldTypes } from '../constants';
+import Modal from '~/vue_shared/components/gl_modal.vue';
+import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import CodeBlock from '~/vue_shared/components/code_block.vue';
+import { fieldTypes } from '../constants';
- export default {
- components: {
- Modal,
- LoadingButton,
- CodeBlock,
+export default {
+ components: {
+ Modal,
+ LoadingButton,
+ CodeBlock,
+ },
+ props: {
+ title: {
+ type: String,
+ required: true,
},
- props: {
- title: {
- type: String,
- required: true,
- },
- modalData: {
- type: Object,
- required: true,
- },
+ modalData: {
+ type: Object,
+ required: true,
},
- fieldTypes,
- };
+ },
+ fieldTypes,
+};
</script>
<template>
<modal
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..7e73ccb6c28 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/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/reports/components/test_issue_body.vue b/app/assets/javascripts/reports/components/test_issue_body.vue
index cd443a49b52..1a87822fcc3 100644
--- a/app/assets/javascripts/reports/components/test_issue_body.vue
+++ b/app/assets/javascripts/reports/components/test_issue_body.vue
@@ -1,28 +1,28 @@
<script>
- import { mapActions } from 'vuex';
+import { mapActions } from 'vuex';
- export default {
- name: 'TestIssueBody',
- props: {
- issue: {
- type: Object,
- required: true,
- },
- // failed || success
- status: {
- type: String,
- required: true,
- },
- isNew: {
- type: Boolean,
- required: false,
- default: false,
- },
+export default {
+ name: 'TestIssueBody',
+ props: {
+ issue: {
+ type: Object,
+ required: true,
},
- methods: {
- ...mapActions(['openModal']),
+ // failed || success
+ status: {
+ type: String,
+ required: true,
},
- };
+ isNew: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ methods: {
+ ...mapActions(['openModal']),
+ },
+};
</script>
<template>
<div class="report-block-list-issue-description prepend-top-5 append-bottom-5">
diff --git a/app/assets/javascripts/reports/store/actions.js b/app/assets/javascripts/reports/store/actions.js
index acabcc1d193..db8ab5ccb80 100644
--- a/app/assets/javascripts/reports/store/actions.js
+++ b/app/assets/javascripts/reports/store/actions.js
@@ -43,9 +43,11 @@ export const fetchReports = ({ state, dispatch }) => {
},
data: state.endpoint,
method: 'getReports',
- successCallback: ({ data, status }) => dispatch('receiveReportsSuccess', {
- data, status,
- }),
+ successCallback: ({ data, status }) =>
+ dispatch('receiveReportsSuccess', {
+ data,
+ status,
+ }),
errorCallback: () => dispatch('receiveReportsError'),
});
diff --git a/app/assets/javascripts/reports/store/index.js b/app/assets/javascripts/reports/store/index.js
index 9d8f7dc3b74..467c692b438 100644
--- a/app/assets/javascripts/reports/store/index.js
+++ b/app/assets/javascripts/reports/store/index.js
@@ -7,9 +7,10 @@ import state from './state';
Vue.use(Vuex);
-export default () => new Vuex.Store({
- actions,
- mutations,
- getters,
- state: state(),
-});
+export default () =>
+ new Vuex.Store({
+ actions,
+ mutations,
+ getters,
+ state: state(),
+ });
diff --git a/app/assets/javascripts/reports/store/mutation_types.js b/app/assets/javascripts/reports/store/mutation_types.js
index 82bda31df5d..599d4862dfe 100644
--- a/app/assets/javascripts/reports/store/mutation_types.js
+++ b/app/assets/javascripts/reports/store/mutation_types.js
@@ -4,4 +4,3 @@ export const REQUEST_REPORTS = 'REQUEST_REPORTS';
export const RECEIVE_REPORTS_SUCCESS = 'RECEIVE_REPORTS_SUCCESS';
export const RECEIVE_REPORTS_ERROR = 'RECEIVE_REPORTS_ERROR';
export const SET_ISSUE_MODAL_DATA = 'SET_ISSUE_MODAL_DATA';
-
diff --git a/app/assets/javascripts/reports/store/mutations.js b/app/assets/javascripts/reports/store/mutations.js
index b88bff97075..2a37f5b74fa 100644
--- a/app/assets/javascripts/reports/store/mutations.js
+++ b/app/assets/javascripts/reports/store/mutations.js
@@ -19,7 +19,6 @@ export default {
state.status = response.status;
state.reports = response.suites;
-
},
[types.RECEIVE_REPORTS_ERROR](state) {
state.isLoading = false;
@@ -36,7 +35,7 @@ export default {
[types.SET_ISSUE_MODAL_DATA](state, payload) {
state.modal.title = payload.issue.name;
- Object.keys(payload.issue).forEach((key) => {
+ Object.keys(payload.issue).forEach(key => {
if (Object.prototype.hasOwnProperty.call(state.modal.data, key)) {
state.modal.data[key] = {
...state.modal.data[key],
diff --git a/app/assets/javascripts/reports/store/state.js b/app/assets/javascripts/reports/store/state.js
index 4cab2e27a16..5484900276c 100644
--- a/app/assets/javascripts/reports/store/state.js
+++ b/app/assets/javascripts/reports/store/state.js
@@ -57,5 +57,4 @@ export default () => ({
},
},
},
-
});
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..0a4583b5861 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()}`,
});
}
}
@@ -251,7 +253,6 @@ export class SearchAutocomplete {
}
getCategoryContents() {
- const userId = gon.current_user_id;
const userName = gon.current_username;
const { projectOptions, groupOptions, dashboardOptions } = gl;
@@ -277,21 +278,21 @@ export class SearchAutocomplete {
const issueItems = [
{
text: s__('SearchAutocomplete|Issues assigned to me'),
- url: `${issuesPath}/?assignee_id=${userId}`,
+ url: `${issuesPath}/?assignee_username=${userName}`,
},
{
text: s__("SearchAutocomplete|Issues I've created"),
- url: `${issuesPath}/?author_id=${userId}`,
+ url: `${issuesPath}/?author_username=${userName}`,
},
];
const mergeRequestItems = [
{
text: s__('SearchAutocomplete|Merge requests assigned to me'),
- url: `${mrPath}/?assignee_id=${userId}`,
+ url: `${mrPath}/?assignee_username=${userName}`,
},
{
text: s__("SearchAutocomplete|Merge requests I've created"),
- url: `${mrPath}/?author_id=${userId}`,
+ url: `${mrPath}/?author_username=${userName}`,
},
];
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..8a48eea5c89 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/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/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index 123c92aff64..cfa7029b388 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -69,7 +69,8 @@ export default {
this.loading = false;
}
- this.mediator.saveAssignees(this.field)
+ this.mediator
+ .saveAssignees(this.field)
.then(setLoadingFalse.bind(this))
.catch(() => {
setLoadingFalse();
diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
index 2b8d6207dea..439e8a69df0 100644
--- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -56,11 +56,7 @@ export default {
.update('issue', { confidential })
.then(() => window.location.reload())
.catch(() => {
- Flash(
- __(
- 'Something went wrong trying to change the confidentiality of this issue',
- ),
- );
+ Flash(__('Something went wrong trying to change the confidentiality of this issue'));
});
},
},
diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
index cdff4105335..48a2b9194aa 100644
--- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
@@ -34,11 +34,7 @@ export default {
required: true,
type: Object,
validator(mediatorObject) {
- return (
- mediatorObject.service &&
- mediatorObject.service.update &&
- mediatorObject.store
- );
+ return mediatorObject.service && mediatorObject.service.update && mediatorObject.store;
},
},
},
@@ -67,8 +63,7 @@ export default {
methods: {
toggleForm() {
- this.mediator.store.isLockDialogOpen = !this.mediator.store
- .isLockDialogOpen;
+ this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen;
},
updateLockedAttribute(locked) {
@@ -79,9 +74,14 @@ export default {
.then(() => window.location.reload())
.catch(() =>
Flash(
- sprintf(__('Something went wrong trying to change the locked state of this %{issuableDisplayName}'), {
- issuableDisplayName: this.issuableDisplayName,
- }),
+ sprintf(
+ __(
+ 'Something went wrong trying to change the locked state of this %{issuableDisplayName}',
+ ),
+ {
+ issuableDisplayName: this.issuableDisplayName,
+ },
+ ),
),
);
},
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
index 286a16f7bbf..43d2f754bac 100644
--- a/app/assets/javascripts/sidebar/components/participants/participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -1,78 +1,80 @@
<script>
- 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 { __, 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/ui';
- export default {
- directives: {
- tooltip,
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ userAvatarImage,
+ GlLoadingIcon,
+ },
+ props: {
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- components: {
- userAvatarImage,
+ participants: {
+ type: Array,
+ required: false,
+ default: () => [],
},
- props: {
- loading: {
- type: Boolean,
- required: false,
- default: false,
- },
- participants: {
- type: Array,
- required: false,
- default: () => [],
- },
- numberOfLessParticipants: {
- type: Number,
- required: false,
- default: 7,
- },
+ numberOfLessParticipants: {
+ type: Number,
+ required: false,
+ default: 7,
},
- data() {
- return {
- isShowingMoreParticipants: false,
- };
+ },
+ data() {
+ return {
+ isShowingMoreParticipants: false,
+ };
+ },
+ computed: {
+ lessParticipants() {
+ return this.participants.slice(0, this.numberOfLessParticipants);
},
- computed: {
- lessParticipants() {
- return this.participants.slice(0, this.numberOfLessParticipants);
- },
- visibleParticipants() {
- return this.isShowingMoreParticipants ? this.participants : this.lessParticipants;
- },
- hasMoreParticipants() {
- return this.participants.length > this.numberOfLessParticipants;
- },
- toggleLabel() {
- let label = '';
- if (this.isShowingMoreParticipants) {
- label = __('- show less');
- } else {
- label = sprintf(__('+ %{moreCount} more'), {
- moreCount: this.participants.length - this.numberOfLessParticipants,
- });
- }
+ visibleParticipants() {
+ return this.isShowingMoreParticipants ? this.participants : this.lessParticipants;
+ },
+ hasMoreParticipants() {
+ return this.participants.length > this.numberOfLessParticipants;
+ },
+ toggleLabel() {
+ let label = '';
+ if (this.isShowingMoreParticipants) {
+ label = __('- show less');
+ } else {
+ label = sprintf(__('+ %{moreCount} more'), {
+ moreCount: this.participants.length - this.numberOfLessParticipants,
+ });
+ }
- return label;
- },
- participantLabel() {
- return sprintf(
- n__('%{count} participant', '%{count} participants', this.participants.length),
- { count: this.loading ? '' : this.participantCount },
- );
- },
- participantCount() {
- return this.participants.length;
- },
+ return label;
+ },
+ participantLabel() {
+ return sprintf(
+ n__('%{count} participant', '%{count} participants', this.participants.length),
+ { count: this.loading ? '' : this.participantCount },
+ );
+ },
+ participantCount() {
+ return this.participants.length;
+ },
+ },
+ methods: {
+ toggleMoreParticipants() {
+ this.isShowingMoreParticipants = !this.isShowingMoreParticipants;
},
- methods: {
- toggleMoreParticipants() {
- this.isShowingMoreParticipants = !this.isShowingMoreParticipants;
- },
- onClickCollapsedIcon() {
- this.$emit('toggleSidebar');
- },
+ onClickCollapsedIcon() {
+ this.$emit('toggleSidebar');
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue b/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue
index 5c1ead1a8ac..4ac515e552a 100644
--- a/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue
@@ -1,23 +1,23 @@
<script>
- import Store from '../../stores/sidebar_store';
- import participants from './participants.vue';
+import Store from '../../stores/sidebar_store';
+import participants from './participants.vue';
- export default {
- components: {
- participants,
+export default {
+ components: {
+ participants,
+ },
+ props: {
+ mediator: {
+ type: Object,
+ required: true,
},
- props: {
- mediator: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- store: new Store(),
- };
- },
- };
+ },
+ data() {
+ return {
+ store: new Store(),
+ };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
index 385717e7c1e..95a2c8cce6e 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
@@ -21,10 +21,9 @@ export default {
},
methods: {
onToggleSubscription() {
- this.mediator.toggleSubscription()
- .catch(() => {
- Flash(__('Error occurred when toggling the notification subscription'));
- });
+ this.mediator.toggleSubscription().catch(() => {
+ Flash(__('Error occurred when toggling the notification subscription'));
+ });
},
},
};
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/collapsed_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
index 1d030c4f67f..259858e4b46 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
@@ -1,111 +1,111 @@
<script>
- import { __, sprintf } from '~/locale';
- import { abbreviateTime } from '~/lib/utils/pretty_time';
- import icon from '~/vue_shared/components/icon.vue';
- import tooltip from '~/vue_shared/directives/tooltip';
+import { __, sprintf } from '~/locale';
+import { abbreviateTime } from '~/lib/utils/datetime_utility';
+import icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
- export default {
- name: 'TimeTrackingCollapsedState',
- components: {
- icon,
+export default {
+ name: 'TimeTrackingCollapsedState',
+ components: {
+ icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ showComparisonState: {
+ type: Boolean,
+ required: true,
},
- directives: {
- tooltip,
+ showSpentOnlyState: {
+ type: Boolean,
+ required: true,
},
- props: {
- showComparisonState: {
- type: Boolean,
- required: true,
- },
- showSpentOnlyState: {
- type: Boolean,
- required: true,
- },
- showEstimateOnlyState: {
- type: Boolean,
- required: true,
- },
- showNoTimeTrackingState: {
- type: Boolean,
- required: true,
- },
- timeSpentHumanReadable: {
- type: String,
- required: false,
- default: '',
- },
- timeEstimateHumanReadable: {
- type: String,
- required: false,
- default: '',
- },
+ showEstimateOnlyState: {
+ type: Boolean,
+ required: true,
},
- computed: {
- timeSpent() {
- return this.abbreviateTime(this.timeSpentHumanReadable);
- },
- timeEstimate() {
- return this.abbreviateTime(this.timeEstimateHumanReadable);
- },
- divClass() {
- if (this.showComparisonState) {
- return 'compare';
- } else if (this.showEstimateOnlyState) {
- return 'estimate-only';
- } else if (this.showSpentOnlyState) {
- return 'spend-only';
- } else if (this.showNoTimeTrackingState) {
- return 'no-tracking';
- }
+ showNoTimeTrackingState: {
+ type: Boolean,
+ required: true,
+ },
+ timeSpentHumanReadable: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ timeEstimateHumanReadable: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ timeSpent() {
+ return this.abbreviateTime(this.timeSpentHumanReadable);
+ },
+ timeEstimate() {
+ return this.abbreviateTime(this.timeEstimateHumanReadable);
+ },
+ divClass() {
+ if (this.showComparisonState) {
+ return 'compare';
+ } else if (this.showEstimateOnlyState) {
+ return 'estimate-only';
+ } else if (this.showSpentOnlyState) {
+ return 'spend-only';
+ } else if (this.showNoTimeTrackingState) {
+ return 'no-tracking';
+ }
+ return '';
+ },
+ spanClass() {
+ if (this.showComparisonState) {
return '';
- },
- spanClass() {
- if (this.showComparisonState) {
- return '';
- } else if (this.showEstimateOnlyState || this.showSpentOnlyState) {
- return 'bold';
- } else if (this.showNoTimeTrackingState) {
- return 'no-value';
- }
+ } else if (this.showEstimateOnlyState || this.showSpentOnlyState) {
+ return 'bold';
+ } else if (this.showNoTimeTrackingState) {
+ return 'no-value';
+ }
- return '';
- },
- text() {
- if (this.showComparisonState) {
- return `${this.timeSpent} / ${this.timeEstimate}`;
- } else if (this.showEstimateOnlyState) {
- return `-- / ${this.timeEstimate}`;
- } else if (this.showSpentOnlyState) {
- return `${this.timeSpent} / --`;
- } else if (this.showNoTimeTrackingState) {
- return 'None';
- }
+ return '';
+ },
+ text() {
+ if (this.showComparisonState) {
+ return `${this.timeSpent} / ${this.timeEstimate}`;
+ } else if (this.showEstimateOnlyState) {
+ return `-- / ${this.timeEstimate}`;
+ } else if (this.showSpentOnlyState) {
+ return `${this.timeSpent} / --`;
+ } else if (this.showNoTimeTrackingState) {
+ return 'None';
+ }
- return '';
- },
- timeTrackedTooltipText() {
- let title;
- if (this.showComparisonState) {
- title = __('Time remaining');
- } else if (this.showEstimateOnlyState) {
- title = __('Estimated');
- } else if (this.showSpentOnlyState) {
- title = __('Time spent');
- }
+ return '';
+ },
+ timeTrackedTooltipText() {
+ let title;
+ if (this.showComparisonState) {
+ title = __('Time remaining');
+ } else if (this.showEstimateOnlyState) {
+ title = __('Estimated');
+ } else if (this.showSpentOnlyState) {
+ title = __('Time spent');
+ }
- return sprintf('%{title}: %{text}', ({ title, text: this.text }));
- },
- tooltipText() {
- return this.showNoTimeTrackingState ? __('Time tracking') : this.timeTrackedTooltipText;
- },
+ return sprintf('%{title}: %{text}', { title, text: this.text });
+ },
+ tooltipText() {
+ return this.showNoTimeTrackingState ? __('Time tracking') : this.timeTrackedTooltipText;
},
- methods: {
- abbreviateTime(timeStr) {
- return abbreviateTime(timeStr);
- },
+ },
+ methods: {
+ abbreviateTime(timeStr) {
+ return abbreviateTime(timeStr);
},
- };
+ },
+};
</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 dc599e1b9fc..87780411c26 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/pretty_time';
+import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
import tooltip from '../../../vue_shared/directives/tooltip';
+import { GlProgressBar } from '@gitlab/ui';
export default {
name: 'TimeTrackingComparisonPane',
+ components: {
+ GlProgressBar,
+ },
directives: {
tooltip,
},
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
index 19ec0f05a26..91909cd49b8 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
@@ -15,16 +15,22 @@ export default {
},
estimateText() {
return sprintf(
- s__('estimateCommand|%{slash_command} will update the estimated time with the latest command.'), {
+ s__(
+ 'estimateCommand|%{slash_command} will update the estimated time with the latest command.',
+ ),
+ {
slash_command: '<code>/estimate</code>',
- }, false,
+ },
+ false,
);
},
spendText() {
return sprintf(
- s__('spendCommand|%{slash_command} will update the sum of the time spent.'), {
+ s__('spendCommand|%{slash_command} will update the sum of the time spent.'),
+ {
slash_command: '<code>/spend</code>',
- }, false,
+ },
+ false,
);
},
},
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
index 8660b0546cf..8e8b9f19b6e 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
@@ -26,7 +26,7 @@ export default {
methods: {
listenForQuickActions() {
$(document).on('ajax:success', '.gfm-form', this.quickActionListened);
- eventHub.$on('timeTrackingUpdated', (data) => {
+ eventHub.$on('timeTrackingUpdated', data => {
this.quickActionListened(null, data);
});
},
@@ -34,9 +34,7 @@ export default {
const subscribedCommands = ['spend_time', 'time_estimate'];
let changedCommands;
if (data !== undefined) {
- changedCommands = data.commands_changes
- ? Object.keys(data.commands_changes)
- : [];
+ changedCommands = data.commands_changes ? Object.keys(data.commands_changes) : [];
} else {
changedCommands = [];
}
diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
index a6b3a674952..7edef35461a 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/ui';
import Icon from '~/vue_shared/components/icon.vue';
@@ -13,6 +14,7 @@ export default {
},
components: {
Icon,
+ GlLoadingIcon,
},
props: {
issuableId: {
@@ -41,9 +43,9 @@ export default {
},
computed: {
buttonClasses() {
- return this.collapsed ?
- 'btn-blank btn-todo sidebar-collapsed-icon dont-change-state' :
- 'btn btn-default btn-todo issuable-header-btn float-right';
+ return this.collapsed
+ ? 'btn-blank btn-todo sidebar-collapsed-icon dont-change-state'
+ : 'btn btn-default btn-todo issuable-header-btn float-right';
},
buttonLabel() {
return this.isTodo ? MARK_TEXT : TODO_TEXT;
diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
index b267422cd97..225ebb61195 100644
--- a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
+++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
@@ -37,7 +37,8 @@ class SidebarMoveIssue {
// Keep the dropdown open after selecting an option
shouldPropagate: false,
data: (searchTerm, callback) => {
- this.mediator.fetchAutocompleteProjects(searchTerm)
+ this.mediator
+ .fetchAutocompleteProjects(searchTerm)
.then(callback)
.catch(() => new window.Flash('An error occurred while fetching projects autocomplete.'));
},
@@ -48,7 +49,7 @@ class SidebarMoveIssue {
</a>
</li>
`,
- clicked: (options) => {
+ clicked: options => {
const project = options.selectedObj;
const selectedProjectId = options.isMarking ? project.id : 0;
this.mediator.setMoveToProjectId(selectedProjectId);
@@ -68,17 +69,12 @@ class SidebarMoveIssue {
onConfirmClicked() {
if (isValidProjectId(this.mediator.store.moveToProjectId)) {
- this.$confirmButton
- .disable()
- .addClass('is-loading');
+ this.$confirmButton.disable().addClass('is-loading');
- this.mediator.moveIssue()
- .catch(() => {
- window.Flash('An error occurred while moving the issue.');
- this.$confirmButton
- .enable()
- .removeClass('is-loading');
- });
+ this.mediator.moveIssue().catch(() => {
+ window.Flash('An error occurred while moving the issue.');
+ this.$confirmButton.enable().removeClass('is-loading');
+ });
}
}
}
diff --git a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
index 87da65a1b1f..1ebdbec7bc9 100644
--- a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
@@ -15,15 +15,16 @@ export default class SidebarMilestone {
components: {
timeTracker,
},
- render: createElement => createElement('timeTracker', {
- props: {
- timeEstimate: parseInt(timeEstimate, 10),
- timeSpent: parseInt(timeSpent, 10),
- humanTimeEstimate,
- humanTimeSpent,
- rootPath: '/',
- },
- }),
+ render: createElement =>
+ createElement('timeTracker', {
+ props: {
+ timeEstimate: parseInt(timeEstimate, 10),
+ timeSpent: parseInt(timeSpent, 10),
+ humanTimeEstimate,
+ humanTimeSpent,
+ rootPath: '/',
+ },
+ }),
});
}
}
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 655bf9198b7..6f8214b18ee 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -22,14 +22,15 @@ function mountAssigneesComponent(mediator) {
components: {
SidebarAssignees,
},
- render: createElement => createElement('sidebar-assignees', {
- props: {
- mediator,
- field: el.dataset.field,
- signedIn: el.hasAttribute('data-signed-in'),
- issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request',
- },
- }),
+ render: createElement =>
+ createElement('sidebar-assignees', {
+ props: {
+ mediator,
+ field: el.dataset.field,
+ signedIn: el.hasAttribute('data-signed-in'),
+ issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request',
+ },
+ }),
});
}
@@ -83,11 +84,12 @@ function mountParticipantsComponent(mediator) {
components: {
sidebarParticipants,
},
- render: createElement => createElement('sidebar-participants', {
- props: {
- mediator,
- },
- }),
+ render: createElement =>
+ createElement('sidebar-participants', {
+ props: {
+ mediator,
+ },
+ }),
});
}
@@ -102,11 +104,12 @@ function mountSubscriptionsComponent(mediator) {
components: {
sidebarSubscriptions,
},
- render: createElement => createElement('sidebar-subscriptions', {
- props: {
- mediator,
- },
- }),
+ render: createElement =>
+ createElement('sidebar-subscriptions', {
+ props: {
+ mediator,
+ },
+ }),
});
}
diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js
index 37c97225bfd..cbe20f761ff 100644
--- a/app/assets/javascripts/sidebar/services/sidebar_service.js
+++ b/app/assets/javascripts/sidebar/services/sidebar_service.js
@@ -22,11 +22,15 @@ export default class SidebarService {
}
update(key, data) {
- return Vue.http.put(this.endpoint, {
- [key]: data,
- }, {
- emulateJSON: true,
- });
+ return Vue.http.put(
+ this.endpoint,
+ {
+ [key]: data,
+ },
+ {
+ emulateJSON: true,
+ },
+ );
}
getProjectsAutocomplete(searchTerm) {
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 6c87287a4c4..fe741dc60cb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
@@ -2,6 +2,7 @@
import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import FilteredSearchDropdown from '~/vue_shared/components/filtered_search_dropdown.vue';
+import { __ } from '~/locale';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import tooltip from '../../vue_shared/directives/tooltip';
import LoadingButton from '../../vue_shared/components/loading_button.vue';
@@ -9,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 {
@@ -20,6 +22,7 @@ export default {
Icon,
TooltipOnTruncate,
FilteredSearchDropdown,
+ ReviewAppLink,
},
directives: {
tooltip,
@@ -30,12 +33,19 @@ export default {
type: Object,
required: true,
},
+ showMetrics: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ deployedTextMap: {
+ running: __('Deploying to'),
+ success: __('Deployed to'),
+ failed: __('Failed to deploy to'),
},
data() {
- const features = window.gon.features || {};
return {
isStopping: false,
- enableCiEnvironmentsStatusChanges: features.ciEnvironmentsStatusChanges,
};
},
computed: {
@@ -54,10 +64,27 @@ export default {
hasMetrics() {
return !!this.deployment.metrics_url;
},
+ 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;
+ },
+ showMemoryUsage() {
+ return this.hasMetrics && this.showMetrics;
+ },
},
methods: {
stopEnvironment() {
- const msg = 'Are you sure you want to stop this environment?';
+ const msg = __('Are you sure you want to stop this environment?');
const isConfirmed = confirm(msg); // eslint-disable-line
if (isConfirmed) {
@@ -87,10 +114,10 @@ export default {
<div class="ci-widget media">
<div class="media-body">
<div class="deploy-body">
- <div class="deployment-info">
+ <div class="js-deployment-info deployment-info">
<template v-if="hasDeploymentMeta">
<span>
- Deployed to
+ {{ deployedText }}
</span>
<tooltip-on-truncate
:title="deployment.name"
@@ -116,7 +143,7 @@ export default {
{{ deployTimeago }}
</span>
<memory-usage
- v-if="hasMetrics"
+ v-if="showMemoryUsage"
:metrics-url="deployment.metrics_url"
:metrics-monitoring-url="deployment.metrics_monitoring_url"
/>
@@ -124,7 +151,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"
@@ -134,18 +161,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
@@ -168,28 +187,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..060361a6516 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/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..409fc2f2db4 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/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..fb9ca897844 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/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..5a7f6cc36fa 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/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/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
deleted file mode 100644
index a23496c6bf5..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
- * This file is the centerpiece of an attempt to reduce potential conflicts
- * between the CE and EE versions of the MR widget. EE additions to the MR widget should
- * be contained in the ee/vue_merge_request_widget directory, and should **extend**
- * rather than mutate CE MR Widget code.
- *
- * This file should be the only source of conflicts between EE and CE. EE-only components should
- * imported directly where they are needed, and import paths for EE extensions of CE components
- * should overwrite import paths **without** changing the order of dependencies listed here.
- */
-
-export { default as Vue } from 'vue';
-export { default as SmartInterval } from '~/smart_interval';
-export { default as WidgetHeader } from './components/mr_widget_header.vue';
-export { default as WidgetMergeHelp } from './components/mr_widget_merge_help.vue';
-export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue';
-export { default as Deployment } from './components/deployment.vue';
-export { default as WidgetRelatedLinks } from './components/mr_widget_related_links.vue';
-export { default as MergedState } from './components/states/mr_widget_merged.vue';
-export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue';
-export { default as ClosedState } from './components/states/mr_widget_closed.vue';
-export { default as MergingState } from './components/states/mr_widget_merging.vue';
-export { default as WorkInProgressState } from './components/states/work_in_progress.vue';
-export { default as ArchivedState } from './components/states/mr_widget_archived.vue';
-export { default as ConflictsState } from './components/states/mr_widget_conflicts.vue';
-export { default as NothingToMergeState } from './components/states/nothing_to_merge.vue';
-export { default as MissingBranchState } from './components/states/mr_widget_missing_branch.vue';
-export { default as NotAllowedState } from './components/states/mr_widget_not_allowed.vue';
-export { default as ReadyToMergeState } from './components/states/ready_to_merge.vue';
-export { default as ShaMismatchState } from './components/states/sha_mismatch.vue';
-export { default as UnresolvedDiscussionsState } from './components/states/unresolved_discussions.vue';
-export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked.vue';
-export { default as PipelineFailedState } from './components/states/pipeline_failed.vue';
-export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds.vue';
-export { default as RebaseState } from './components/states/mr_widget_rebase.vue';
-export { default as AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed.vue';
-export { default as CheckingState } from './components/states/mr_widget_checking.vue';
-export { default as MRWidgetStore } from './stores/mr_widget_store';
-export { default as MRWidgetService } from './services/mr_widget_service';
-export { default as eventHub } from './event_hub';
-export { default as getStateKey } from './stores/get_state_key';
-export { default as stateMaps } from './stores/state_maps';
-export { default as SquashBeforeMerge } from './components/states/squash_before_merge.vue';
-export { default as notify } from '../lib/utils/notify';
-export { default as SourceBranchRemovalStatus } from './components/source_branch_removal_status.vue';
-
-export { default as mrWidgetOptions } from './mr_widget_options.vue';
diff --git a/app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js
new file mode 100644
index 00000000000..8780aa4bd1c
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js
@@ -0,0 +1,3 @@
+import MRWidgetOptions from './mr_widget_options.vue';
+
+export default MRWidgetOptions;
diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js
index cc6e620f365..60cebbfc2b2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/index.js
@@ -1,4 +1,5 @@
-import { Vue, mrWidgetOptions } from './dependencies';
+import Vue from 'vue';
+import MrWidgetOptions from './ee_switch_mr_widget_options';
import Translate from '../vue_shared/translate';
Vue.use(Translate);
@@ -6,7 +7,7 @@ Vue.use(Translate);
export default () => {
gl.mrWidgetData.gitlabLogo = gon.gitlab_logo;
- const vm = new Vue(mrWidgetOptions);
+ const vm = new Vue(MrWidgetOptions);
window.gl.mrWidget = {
checkStatus: vm.checkStatus,
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 8180f13a7cb..3b840540657 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
@@ -1,40 +1,40 @@
<script>
+import _ from 'underscore';
+import { __ } from '~/locale';
import Project from '~/pages/projects/project';
import SmartInterval from '~/smart_interval';
import createFlash from '../flash';
-import {
- WidgetHeader,
- WidgetMergeHelp,
- WidgetPipeline,
- Deployment,
- WidgetRelatedLinks,
- MergedState,
- ClosedState,
- MergingState,
- RebaseState,
- WorkInProgressState,
- ArchivedState,
- ConflictsState,
- NothingToMergeState,
- MissingBranchState,
- NotAllowedState,
- ReadyToMergeState,
- ShaMismatchState,
- UnresolvedDiscussionsState,
- PipelineBlockedState,
- PipelineFailedState,
- FailedToMerge,
- MergeWhenPipelineSucceedsState,
- AutoMergeFailed,
- CheckingState,
- MRWidgetStore,
- MRWidgetService,
- eventHub,
- stateMaps,
- SquashBeforeMerge,
- notify,
- SourceBranchRemovalStatus,
-} from './dependencies';
+import WidgetHeader from './components/mr_widget_header.vue';
+import WidgetMergeHelp from './components/mr_widget_merge_help.vue';
+import WidgetPipeline from './components/mr_widget_pipeline.vue';
+import Deployment from './components/deployment.vue';
+import WidgetRelatedLinks from './components/mr_widget_related_links.vue';
+import MergedState from './components/states/mr_widget_merged.vue';
+import ClosedState from './components/states/mr_widget_closed.vue';
+import MergingState from './components/states/mr_widget_merging.vue';
+import RebaseState from './components/states/mr_widget_rebase.vue';
+import WorkInProgressState from './components/states/work_in_progress.vue';
+import ArchivedState from './components/states/mr_widget_archived.vue';
+import ConflictsState from './components/states/mr_widget_conflicts.vue';
+import NothingToMergeState from './components/states/nothing_to_merge.vue';
+import MissingBranchState from './components/states/mr_widget_missing_branch.vue';
+import NotAllowedState from './components/states/mr_widget_not_allowed.vue';
+import ReadyToMergeState from './components/states/ready_to_merge.vue';
+import ShaMismatchState from './components/states/sha_mismatch.vue';
+import UnresolvedDiscussionsState from './components/states/unresolved_discussions.vue';
+import PipelineBlockedState from './components/states/mr_widget_pipeline_blocked.vue';
+import PipelineFailedState from './components/states/pipeline_failed.vue';
+import FailedToMerge from './components/states/mr_widget_failed_to_merge.vue';
+import MergeWhenPipelineSucceedsState from './components/states/mr_widget_merge_when_pipeline_succeeds.vue';
+import AutoMergeFailed from './components/states/mr_widget_auto_merge_failed.vue';
+import CheckingState from './components/states/mr_widget_checking.vue';
+import MRWidgetStore from './stores/ee_switch_mr_widget_store';
+import MRWidgetService from './services/ee_switch_mr_widget_service';
+import eventHub from './event_hub';
+import stateMaps from './stores/ee_switch_state_maps';
+import SquashBeforeMerge from './components/states/squash_before_merge.vue';
+import notify from '~/lib/utils/notify';
+import SourceBranchRemovalStatus from './components/source_branch_removal_status.vue';
import GroupedTestReportsApp from '../reports/components/grouped_test_reports_app.vue';
import { setFaviconOverlay } from '../lib/utils/common_utils';
@@ -82,6 +82,7 @@ export default {
const service = this.createService(store);
return {
mr: store,
+ state: store.state,
service,
};
},
@@ -105,6 +106,17 @@ export default {
(!this.mr.isNothingToMergeState && !this.mr.isMergedState)
);
},
+ shouldRenderMergedPipeline() {
+ return this.mr.state === 'merged' && !_.isEmpty(this.mr.mergePipeline);
+ },
+ },
+ watch: {
+ state(newVal, oldVal) {
+ if (newVal !== oldVal && this.shouldRenderMergedPipeline) {
+ // init polling
+ this.initPostMergeDeploymentsPolling();
+ }
+ },
},
created() {
this.initPolling();
@@ -114,11 +126,19 @@ export default {
mounted() {
this.setFaviconHelper();
this.initDeploymentsPolling();
+
+ if (this.shouldRenderMergedPipeline) {
+ this.initPostMergeDeploymentsPolling();
+ }
},
beforeDestroy() {
eventHub.$off('mr.discussion.updated', this.checkStatus);
this.pollingInterval.destroy();
this.deploymentsInterval.destroy();
+
+ if (this.postMergeDeploymentsInterval) {
+ this.postMergeDeploymentsInterval.destroy();
+ }
},
methods: {
createService(store) {
@@ -148,7 +168,13 @@ export default {
cb.call(null, data);
}
})
- .catch(() => createFlash('Something went wrong. Please try again.'));
+ .catch(() => createFlash(__('Something went wrong. Please try again.')));
+ },
+ setFaviconHelper() {
+ if (this.mr.ciStatusFaviconPath) {
+ return setFaviconOverlay(this.mr.ciStatusFaviconPath);
+ }
+ return Promise.resolve();
},
initPolling() {
this.pollingInterval = new SmartInterval({
@@ -160,8 +186,14 @@ export default {
});
},
initDeploymentsPolling() {
- this.deploymentsInterval = new SmartInterval({
- callback: this.fetchDeployments,
+ this.deploymentsInterval = this.deploymentsPoll(this.fetchPreMergeDeployments);
+ },
+ initPostMergeDeploymentsPolling() {
+ this.postMergeDeploymentsInterval = this.deploymentsPoll(this.fetchPostMergeDeployments);
+ },
+ deploymentsPoll(callback) {
+ return new SmartInterval({
+ callback,
startingInterval: 30000,
maxInterval: 120000,
hiddenInterval: 240000,
@@ -169,26 +201,33 @@ export default {
immediateExecution: true,
});
},
- setFaviconHelper() {
- if (this.mr.ciStatusFaviconPath) {
- return setFaviconOverlay(this.mr.ciStatusFaviconPath);
- }
- return Promise.resolve();
+ fetchDeployments(target) {
+ return this.service.fetchDeployments(target);
},
- fetchDeployments() {
- return this.service
- .fetchDeployments()
- .then(res => res.data)
- .then(data => {
+ fetchPreMergeDeployments() {
+ return this.fetchDeployments()
+ .then(({ data }) => {
if (data.length) {
this.mr.deployments = data;
}
})
- .catch(() => {
- createFlash(
- 'Something went wrong while fetching the environments for this merge request. Please try again.',
- );
- });
+ .catch(() => this.throwDeploymentsError());
+ },
+ fetchPostMergeDeployments() {
+ return this.fetchDeployments('merge_commit')
+ .then(({ data }) => {
+ if (data.length) {
+ this.mr.postMergeDeployments = data;
+ }
+ })
+ .catch(() => this.throwDeploymentsError());
+ },
+ throwDeploymentsError() {
+ createFlash(
+ __(
+ 'Something went wrong while fetching the environments for this merge request. Please try again.',
+ ),
+ );
},
fetchActionsContent() {
this.service
@@ -201,7 +240,7 @@ export default {
Project.initRefSwitcher();
}
})
- .catch(() => createFlash('Something went wrong. Please try again.'));
+ .catch(() => createFlash(__('Something went wrong. Please try again.')));
},
handleNotification(data) {
if (data.ci_status === this.mr.ciStatus) return;
@@ -266,11 +305,14 @@ 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"
- :key="deployment.id"
+ :key="`pre-merge-deploy-${deployment.id}`"
+ class="js-pre-merge-deploy"
:deployment="deployment"
+ :show-metrics="false"
/>
<div class="mr-section-container">
<grouped-test-reports-app
@@ -310,5 +352,24 @@ export default {
<mr-widget-merge-help />
</div>
</div>
+
+ <template v-if="shouldRenderMergedPipeline">
+ <mr-widget-pipeline
+ class="js-post-merge-pipeline prepend-top-default"
+ :pipeline="mr.mergePipeline"
+ :ci-status="mr.ciStatus"
+ :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"
+ :key="`post-merge-deploy-${postMergeDeployment.id}`"
+ :deployment="postMergeDeployment"
+ :show-metrics="true"
+ class="js-post-deployment"
+ />
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js
new file mode 100644
index 00000000000..ea2aabb78fe
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js
@@ -0,0 +1,3 @@
+import MRWidgetService from './mr_widget_service';
+
+export default MRWidgetService;
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 fecbfec2214..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
@@ -21,8 +21,12 @@ export default class MRWidgetService {
return axios.delete(this.endpoints.sourceBranchPath);
}
- fetchDeployments() {
- return axios.get(this.endpoints.ciEnvironmentsStatusPath);
+ fetchDeployments(targetParam) {
+ return axios.get(this.endpoints.ciEnvironmentsStatusPath, {
+ params: {
+ environment_target: targetParam,
+ },
+ });
}
poll() {
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js
new file mode 100644
index 00000000000..ebef30e3eab
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js
@@ -0,0 +1,3 @@
+import getStateKey from './get_state_key';
+
+export default getStateKey;
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js
new file mode 100644
index 00000000000..92a07c53f2d
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js
@@ -0,0 +1,3 @@
+import MergeRequestStore from './mr_widget_store';
+
+export default MergeRequestStore;
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js
new file mode 100644
index 00000000000..50cf9503ea7
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js
@@ -0,0 +1,3 @@
+import stateMaps from './state_maps';
+
+export default stateMaps;
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 672e5280b5e..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
@@ -1,5 +1,5 @@
import Timeago from 'timeago.js';
-import { getStateKey } from '../dependencies';
+import getStateKey from './ee_switch_get_state_key';
import { stateKey } from './state_maps';
import { formatDate } from '../../lib/utils/datetime_utility';
@@ -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;
@@ -32,7 +33,9 @@ export default class MergeRequestStore {
this.commitsCount = data.commits_count;
this.divergedCommitsCount = data.diverged_commits_count;
this.pipeline = data.pipeline || {};
+ this.mergePipeline = data.merge_pipeline || {};
this.deployments = this.deployments || data.deployments || [];
+ this.postMergeDeployments = this.postMergeDeployments || [];
this.initRebase(data);
if (data.issues_links) {
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..c5fbaf87b00 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/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..b0962684430 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/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..671b4909839 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/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..420bd25b496 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/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/download_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
index a07d63a495d..97bdd9915c5 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
@@ -1,11 +1,11 @@
<script>
-import { Link } from '@gitlab-org/gitlab-ui';
+import { GlLink } from '@gitlab/ui';
import Icon from '../../icon.vue';
import { numberToHumanSize } from '../../../../lib/utils/number_utils';
export default {
components: {
- 'gl-link': Link,
+ GlLink,
Icon,
},
props: {
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/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
index 807e049caf6..a084cfdf612 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
@@ -2,14 +2,14 @@
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import $ from 'jquery';
-import { SkeletonLoading } from '@gitlab-org/gitlab-ui';
+import { GlSkeletonLoading } from '@gitlab/ui';
const { CancelToken } = axios;
let axiosSource;
export default {
components: {
- SkeletonLoading,
+ GlSkeletonLoading,
},
props: {
content: {
@@ -81,7 +81,7 @@ export default {
<div
ref="markdown-preview"
class="md md-previewer">
- <skeleton-loading v-if="isLoading" />
+ <gl-skeleton-loading v-if="isLoading" />
<div
v-else
v-html="previewContent">
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..0f4effda79f 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/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..545be568ad3 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/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
new file mode 100644
index 00000000000..97f7998f461
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/gl_countdown.vue
@@ -0,0 +1,53 @@
+<script>
+import { calculateRemainingMilliseconds, formatTime } from '~/lib/utils/datetime_utility';
+import { GlTooltipDirective } from '@gitlab/ui';
+
+/**
+ * Counts down to a given end date.
+ */
+export default {
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ endDateString: {
+ type: String,
+ required: true,
+ validator(value) {
+ return !Number.isNaN(new Date(value).getTime());
+ },
+ },
+ },
+
+ data() {
+ return {
+ remainingTime: formatTime(0),
+ countdownUpdateIntervalId: null,
+ };
+ },
+
+ mounted() {
+ const updateRemainingTime = () => {
+ const remainingMilliseconds = calculateRemainingMilliseconds(this.endDateString);
+ this.remainingTime = formatTime(remainingMilliseconds);
+ };
+
+ updateRemainingTime();
+ this.countdownUpdateIntervalId = window.setInterval(updateRemainingTime, 1000);
+ },
+
+ beforeDestroy() {
+ window.clearInterval(this.countdownUpdateIntervalId);
+ },
+};
+</script>
+
+<template>
+ <time
+ v-gl-tooltip
+ :datetime="endDateString"
+ :title="endDateString"
+ >
+ {{ remainingTime }}
+ </time>
+</template>
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..1a91a8b81e3 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/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..9bae8a32a8c 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/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..ca9e57a9b00 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/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.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index feb7b8f227e..3cb48023002 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -1,9 +1,9 @@
<script>
-import { Link } from '@gitlab-org/gitlab-ui';
+import { GlLink } from '@gitlab/ui';
export default {
components: {
- 'gl-link': Link,
+ GlLink,
},
props: {
markdownDocsPath: {
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..13af4b627de 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/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 1d9c9220469..2dcd161b4fb 100644
--- a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
@@ -1,23 +1,23 @@
<script>
-import { SkeletonLoading } from '@gitlab-org/gitlab-ui';
+import { GlSkeletonLoading } from '@gitlab/ui';
export default {
name: 'SkeletonNote',
components: {
- SkeletonLoading,
+ GlSkeletonLoading,
},
};
</script>
<template>
- <li class="timeline-entry note">
+ <li class="timeline-entry note note-wrapper">
<div class="timeline-entry-inner">
<div class="timeline-icon">
</div>
<div class="timeline-content">
<div class="note-header"></div>
<div class="note-body">
- <skeleton-loading />
+ <gl-skeleton-loading />
</div>
</div>
</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..0b44f8578cb 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/ui';
import { s__ } from '../../locale';
export default {
+ components: {
+ GlPagination,
+ },
props: {
change: {
type: Function,
diff --git a/app/assets/javascripts/vue_shared/components/pikaday.vue b/app/assets/javascripts/vue_shared/components/pikaday.vue
index 782d8e3abf6..26c99aecae4 100644
--- a/app/assets/javascripts/vue_shared/components/pikaday.vue
+++ b/app/assets/javascripts/vue_shared/components/pikaday.vue
@@ -1,6 +1,6 @@
<script>
import Pikaday from 'pikaday';
-import { parsePikadayDate, pikadayToString } from '../../lib/utils/datefix';
+import { parsePikadayDate, pikadayToString } from '~/lib/utils/datetime_utility';
export default {
name: 'DatePicker',
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 7f1eb6bcec4..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,34 +1,50 @@
<script>
- export default {
- name: 'CollapsedCalendarIcon',
- props: {
- containerClass: {
- type: String,
- required: false,
- default: '',
- },
- text: {
- type: String,
- required: false,
- default: '',
- },
- showIcon: {
- type: Boolean,
- required: false,
- default: true,
- },
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ name: 'CollapsedCalendarIcon',
+ directives: {
+ tooltip,
+ },
+ 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: '',
},
- methods: {
- click() {
- this.$emit('click');
- },
+ },
+ methods: {
+ click() {
+ this.$emit('click');
},
- };
+ },
+};
</script>
<template>
<div
+ v-tooltip
:class="containerClass"
+ :title="tooltipText"
+ data-container="body"
+ data-placement="left"
+ data-html="true"
+ data-boundary="viewport"
@click="click"
>
<i
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 dac438a702d..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,88 +1,87 @@
<script>
- import { dateInWords } from '../../../lib/utils/datetime_utility';
- import toggleSidebar from './toggle_sidebar.vue';
- 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: {
- toggleSidebar,
- collapsedCalendarIcon,
+export default {
+ name: 'SidebarCollapsedGroupedDatePicker',
+ components: {
+ collapsedCalendarIcon,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ collapsed: {
+ type: Boolean,
+ required: false,
+ default: true,
},
- props: {
- collapsed: {
- type: Boolean,
- required: false,
- default: true,
- },
- showToggleSidebar: {
- type: Boolean,
- required: false,
- default: false,
- },
- 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 `block 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';
- },
+ if (date) {
+ return [defaultText, dateText].join('<br />');
+ }
+ return __('Start and due date');
},
- };
+ },
+};
</script>
<template>
<div class="block sidebar-grouped-item">
- <div
- v-if="showToggleSidebar"
- class="issuable-sidebar-header"
- >
- <toggle-sidebar
- :collapsed="collapsed"
- @toggle="toggleSidebar"
- />
- </div>
<collapsed-calendar-icon
v-if="showMinDateBlock"
:container-class="iconClass"
+ :tooltip-text="tooltipText('min')"
@click="toggleSidebar"
>
<span class="sidebar-collapsed-value">
@@ -99,7 +98,7 @@
<collapsed-calendar-icon
v-if="maxDate"
:container-class="iconClass"
- :show-icon="!minDate"
+ :tooltip-text="tooltipText('max')"
@click="toggleSidebar"
>
<span class="sidebar-collapsed-value">
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..6aa880603b9 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/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..98b8b6460fe 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/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..5d1c92c3b3d 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/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..c78d98ccd9e 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/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 14cb44b8619..6dd519ea56d 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,18 +17,17 @@
*/
-import { Link } from '@gitlab-org/gitlab-ui';
+import { GlLink, GlTooltipDirective } from '@gitlab/ui';
import userAvatarImage from './user_avatar_image.vue';
-import tooltip from '../../directives/tooltip';
export default {
name: 'UserAvatarLink',
components: {
- 'gl-link': Link,
+ GlLink,
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/directives/tooltip.js b/app/assets/javascripts/vue_shared/directives/tooltip.js
index 4f2412ce520..549d27e96d9 100644
--- a/app/assets/javascripts/vue_shared/directives/tooltip.js
+++ b/app/assets/javascripts/vue_shared/directives/tooltip.js
@@ -9,6 +9,14 @@ export default {
componentUpdated(el) {
$(el).tooltip('_fixTitle');
+
+ // update visible tooltips
+ const tooltipInstance = $(el).data('bs.tooltip');
+ const tip = tooltipInstance.getTipElement();
+ tooltipInstance.setElementContent(
+ $(tip.querySelectorAll('.tooltip-inner')),
+ tooltipInstance.getTitle(),
+ );
},
unbind(el) {
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..037a5adfb7e 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;
@@ -416,7 +415,6 @@ span.idiff {
}
.preview-container {
- height: 100%;
overflow: auto;
.file-container {
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 1c84baf68ed..9837b1a6bd0 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -250,6 +250,101 @@
max-width: 100%;
}
+/*
+* Mixin that handles the container for the job logs (CI/CD and kubernetes pod logs)
+*/
+@mixin build-trace {
+ background: $black;
+ color: $gray-darkest;
+ white-space: pre;
+ overflow-x: auto;
+ font-size: 12px;
+ border-radius: 0;
+ border: 0;
+ padding: $grid-size;
+
+ .bash {
+ display: block;
+ }
+
+ &.build-trace-rounded {
+ border-radius: $border-radius-base;
+ }
+}
+
+@mixin build-trace-top-bar($height) {
+ height: $height;
+ min-height: $height;
+ background: $gray-light;
+ border: 1px solid $border-color;
+ color: $gl-text-color;
+ position: sticky;
+ position: -webkit-sticky;
+ top: $header-height;
+ padding: $grid-size;
+
+ .with-performance-bar & {
+ top: $header-height + $performance-bar-height;
+ }
+}
+
+/*
+* 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, $svg-display: 'block', $svg-top: '2px') {
+ display: flex;
+ font-size: $control-font-size;
+ justify-content: $flex-direction;
+ align-items: center;
+ align-self: baseline;
+ @if $with-grow {
+ flex-grow: $flex-grow-size;
+ }
+
+ svg {
+ width: 15px;
+ height: 15px;
+ display: $svg-display;
+ fill: $gl-text-color;
+ top: $svg-top;
+ }
+
+ .controllers-buttons {
+ color: $gl-text-color;
+ margin: 0 $grid-size;
+
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+
+ .btn-scroll.animate {
+ .first-triangle {
+ animation: blinking-scroll-button 1s ease infinite;
+ animation-delay: 0.3s;
+ }
+
+ .second-triangle {
+ animation: blinking-scroll-button 1s ease infinite;
+ animation-delay: 0.2s;
+ }
+
+ .third-triangle {
+ animation: blinking-scroll-button 1s ease infinite;
+ }
+
+ &:disabled {
+ opacity: 1;
+ }
+ }
+
+ .btn-scroll:disabled,
+ .btn-refresh:disabled {
+ opacity: 0.35;
+ cursor: not-allowed;
+ }
+}
+
@mixin build-loader-animation {
position: relative;
white-space: initial;
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/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 6d891e21556..e261bd7c0ca 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -34,7 +34,7 @@
margin-bottom: 0;
}
- *:first-child:not(.katex-display) {
+ *:first-child {
margin-top: 0;
}
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 227f49ec595..81cb519883b 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -50,35 +50,33 @@
position: relative;
}
- .build-trace {
- background: $black;
- color: $gray-darkest;
- white-space: pre;
- overflow-x: auto;
- font-size: 12px;
- border-radius: 0;
- border: 0;
- padding: $grid-size;
- .bash {
- display: block;
- }
+ .build-trace {
+ @include build-trace();
+ }
- &.build-trace-rounded {
- border-radius: $border-radius-base;
+ .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 {
- height: 35px;
- min-height: 35px;
- background: $gray-light;
- border: 1px solid $border-color;
- color: $gl-text-color;
- position: sticky;
- position: -webkit-sticky;
- top: $header-height;
- padding: $grid-size;
+ @include build-trace-top-bar(35px);
+
+ &.has-archived-block {
+ top: $header-height + $performance-bar-height + 28px;
+ }
&.affix {
top: $header-height;
@@ -116,49 +114,7 @@
}
.controllers {
- display: flex;
- justify-content: center;
- align-items: center;
-
- svg {
- height: 15px;
- display: block;
- fill: $gl-text-color;
- }
-
- .controllers-buttons {
- color: $gl-text-color;
- margin: 0 $grid-size;
-
- &:last-child {
- margin-right: 0;
- }
- }
-
- .btn-scroll.animate {
- .first-triangle {
- animation: blinking-scroll-button 1s ease infinite;
- animation-delay: 0.3s;
- }
-
- .second-triangle {
- animation: blinking-scroll-button 1s ease infinite;
- animation-delay: 0.2s;
- }
-
- .third-triangle {
- animation: blinking-scroll-button 1s ease infinite;
- }
-
- &:disabled {
- opacity: 1;
- }
- }
-
- .btn-scroll:disabled {
- opacity: 0.35;
- cursor: not-allowed;
- }
+ @include build-controllers(15px, center, false, 0, inline, 0);
}
}
@@ -183,12 +139,8 @@
}
.with-performance-bar .build-page {
- .top-bar {
+ .top-bar.affix {
top: $header-height + $performance-bar-height;
-
- &.affix {
- top: $header-height + $performance-bar-height;
- }
}
}
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..3c7bf0b0e46 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,17 @@
background-repeat: repeat;
}
+ .diff-file-discussions + .discussion-form {
+ padding: $gl-padding;
+
+ &::before {
+ width: auto;
+ margin-left: -$gl-padding;
+ margin-right: -$gl-padding;
+ margin-bottom: $gl-padding;
+ }
+ }
+
.notes {
position: relative;
}
@@ -855,7 +864,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 +879,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 +921,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 +956,8 @@
}
}
-.image-diff-avatar-link {
+.image-diff-avatar-link,
+.user-avatar-link {
position: relative;
.badge.badge-pill,
@@ -1073,3 +1086,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..38851de6401 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 {
@@ -733,6 +727,10 @@
.issuable-main-info {
flex: 1 auto;
margin-right: 10px;
+
+ .issue-weight-icon {
+ vertical-align: sub;
+ }
}
.issuable-meta {
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..085ff27e6ef 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,17 @@ ul.notes {
}
}
+ .discussion-notes {
+ &:not(:last-child) {
+ margin-bottom: 0;
+ }
+
+ .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 +458,6 @@ ul.notes {
border-left: 0;
&.notes_content {
- background-color: $gray-light;
border-width: 1px 0;
padding: 0;
vertical-align: top;
@@ -349,18 +471,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 +484,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 +550,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 +570,7 @@ ul.notes {
.note-header-info {
min-width: 0;
- padding-bottom: 8px;
+ padding-bottom: $gl-padding-8;
&.discussion {
padding-bottom: 0;
@@ -468,12 +629,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 +754,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 +790,7 @@ ul.notes {
}
.btn {
- svg path {
+ svg {
fill: $gray-darkest;
}
@@ -659,7 +815,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 +931,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/admin/background_jobs_controller.rb b/app/controllers/admin/background_jobs_controller.rb
index 7701f2e645b..fc877142418 100644
--- a/app/controllers/admin/background_jobs_controller.rb
+++ b/app/controllers/admin/background_jobs_controller.rb
@@ -1,9 +1,4 @@
# frozen_string_literal: true
class Admin::BackgroundJobsController < Admin::ApplicationController
- def show
- ps_output, _ = Gitlab::Popen.popen(%W(ps ww -U #{Gitlab.config.gitlab.user} -o pid,pcpu,pmem,stat,start,command))
- @sidekiq_processes = ps_output.split("\n").grep(/sidekiq \d+\.\d+\.\d+/)
- @concurrency = Sidekiq.options[:concurrency]
- end
end
diff --git a/app/controllers/admin/impersonation_tokens_controller.rb b/app/controllers/admin/impersonation_tokens_controller.rb
index f5825ecb19a..706bcc1e549 100644
--- a/app/controllers/admin/impersonation_tokens_controller.rb
+++ b/app/controllers/admin/impersonation_tokens_controller.rb
@@ -11,6 +11,7 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController
@impersonation_token = finder.build(impersonation_token_params)
if @impersonation_token.save
+ PersonalAccessToken.redis_store!(current_user.id, @impersonation_token.token)
redirect_to admin_user_impersonation_tokens_path, notice: "A new impersonation token has been created."
else
set_index_vars
@@ -53,6 +54,8 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController
@impersonation_token ||= finder.build
@inactive_impersonation_tokens = finder(state: 'inactive').execute
@active_impersonation_tokens = finder(state: 'active').execute.order(:expires_at)
+
+ @new_impersonation_token = PersonalAccessToken.redis_getdel(current_user.id)
end
# rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index eeabcc0c9bb..b839da7770d 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -46,6 +46,8 @@ class ApplicationController < ActionController::Base
:git_import_enabled?, :gitlab_project_import_enabled?,
:manifest_import_enabled?
+ DEFAULT_GITLAB_CACHE_CONTROL = "#{ActionDispatch::Http::Cache::Response::DEFAULT_CACHE_CONTROL}, no-store".freeze
+
rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception)
render "errors/encoding", layout: "errors", status: 500
@@ -179,11 +181,11 @@ class ApplicationController < ActionController::Base
Ability.allowed?(object, action, subject)
end
- def access_denied!(message = nil)
+ def access_denied!(message = nil, status = nil)
# If we display a custom access denied message to the user, we don't want to
# hide existence of the resource, rather tell them they cannot access it using
# the provided message
- status = message.present? ? :forbidden : :not_found
+ status ||= message.present? ? :forbidden : :not_found
respond_to do |format|
format.any { head status }
@@ -244,6 +246,13 @@ class ApplicationController < ActionController::Base
headers['X-XSS-Protection'] = '1; mode=block'
headers['X-UA-Compatible'] = 'IE=edge'
headers['X-Content-Type-Options'] = 'nosniff'
+
+ if current_user
+ # Adds `no-store` to the DEFAULT_CACHE_CONTROL, to prevent security
+ # concerns due to caching private data.
+ headers['Cache-Control'] = DEFAULT_GITLAB_CACHE_CONTROL
+ headers["Pragma"] = "no-cache" # HTTP 1.0 compatibility
+ end
end
def validate_user_service_ticket!
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/boards_responses.rb b/app/controllers/concerns/boards_responses.rb
index b7e4f9b81f1..3cdf4ddf8bb 100644
--- a/app/controllers/concerns/boards_responses.rb
+++ b/app/controllers/concerns/boards_responses.rb
@@ -50,7 +50,10 @@ module BoardsResponses
end
def authorize_create_issue
- authorize_action_for!(project, :admin_issue)
+ list = List.find(issue_params[:list_id])
+ action = list.backlog? ? :create_issue : :admin_issue
+
+ authorize_action_for!(project, action)
end
def authorize_admin_list
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 5217b4be928..34a8c50fcbd 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -81,36 +81,36 @@ module IssuableCollections
end
def issuable_finder_for(finder_class)
- finder_class.new(current_user, filter_params)
+ finder_class.new(current_user, finder_options)
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
- # rubocop: disable CodeReuse/ActiveRecord
- def filter_params
- set_sort_order_from_cookie
- set_default_state
+ def finder_options
+ params[:state] = default_state if params[:state].blank?
- # Skip irrelevant Rails routing params
- @filter_params = params.dup.except(:controller, :action, :namespace_id)
- @filter_params[:sort] ||= default_sort_order
+ options = {
+ scope: params[:scope],
+ state: params[:state],
+ sort: set_sort_order_from_cookie || default_sort_order
+ }
- @sort = @filter_params[:sort]
+ # Used by view to highlight active option
+ @sort = options[:sort]
if @project
- @filter_params[:project_id] = @project.id
+ options[:project_id] = @project.id
elsif @group
- @filter_params[:group_id] = @group.id
- @filter_params[:include_subgroups] = true
- @filter_params[:use_cte_for_search] = true
+ options[:group_id] = @group.id
+ options[:include_subgroups] = true
+ options[:use_cte_for_search] = true
end
- @filter_params.permit(finder_type.valid_params)
+ params.permit(finder_type.valid_params).merge(options)
end
- # rubocop: enable CodeReuse/ActiveRecord
# rubocop:enable Gitlab/ModuleWithInstanceVariables
- def set_default_state
- params[:state] = 'opened' if params[:state].blank?
+ def default_state
+ 'opened'
end
def set_sort_order_from_cookie
@@ -121,7 +121,7 @@ module IssuableCollections
sort_value = update_cookie_value(sort_param)
set_secure_cookie(remember_sorting_key, sort_value)
- params[:sort] = sort_value
+ sort_value
end
def remember_sorting_key
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/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb
index 285f2c3a8a0..ed10f32512e 100644
--- a/app/controllers/concerns/merge_requests_action.rb
+++ b/app/controllers/concerns/merge_requests_action.rb
@@ -19,7 +19,7 @@ module MergeRequestsAction
(MergeRequestsFinder if action_name == 'merge_requests')
end
- def filter_params
+ def finder_options
super.merge(non_archived: true)
end
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/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb
index 6e17bc212e4..3802aa5f40f 100644
--- a/app/controllers/dashboard/milestones_controller.rb
+++ b/app/controllers/dashboard/milestones_controller.rb
@@ -4,12 +4,13 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
include MilestoneActions
before_action :projects
+ before_action :groups, only: :index
before_action :milestone, only: [:show, :merge_requests, :participants, :labels]
def index
respond_to do |format|
format.html do
- @milestone_states = GlobalMilestone.states_count(@projects)
+ @milestone_states = Milestone.states_count(@projects.select(:id), @groups.select(:id))
@milestones = Kaminari.paginate_array(milestones).page(params[:page])
end
format.json do
@@ -42,4 +43,8 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
@milestone = DashboardMilestone.build(@projects, params[:title])
render_404 unless @milestone
end
+
+ def groups
+ @groups ||= GroupsFinder.new(current_user, state_all: true).execute
+ end
end
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index c032fb2efb5..4ce9be44403 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -4,13 +4,6 @@ class DashboardController < Dashboard::ApplicationController
include IssuesAction
include MergeRequestsAction
- FILTER_PARAMS = [
- :author_id,
- :assignee_id,
- :milestone_title,
- :label_name
- ].freeze
-
before_action :event_filter, only: :activity
before_action :projects, only: [:issues, :merge_requests]
before_action :set_show_full_reference, only: [:issues, :merge_requests]
@@ -51,10 +44,13 @@ class DashboardController < Dashboard::ApplicationController
end
def check_filters_presence!
- @no_filters_set = FILTER_PARAMS.none? { |k| params.key?(k) }
+ @no_filters_set = finder_type.scalar_params.none? { |k| params.key?(k) }
return unless @no_filters_set
+ # Call to set selected `state` and `sort` options in view
+ finder_options
+
respond_to do |format|
format.html { render }
format.atom { head :bad_request }
diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb
index 8d259b4052e..cdc6f53df8e 100644
--- a/app/controllers/groups/boards_controller.rb
+++ b/app/controllers/groups/boards_controller.rb
@@ -5,6 +5,7 @@ class Groups::BoardsController < Groups::ApplicationController
before_action :assign_endpoint_vars
before_action :boards, only: :index
+ before_action :redirect_to_recent_board, only: :index
def index
respond_with_boards
@@ -13,6 +14,9 @@ class Groups::BoardsController < Groups::ApplicationController
def show
@board = boards.find(params[:id])
+ # add/update the board in the recent visited table
+ Boards::Visits::CreateService.new(@board.group, current_user).execute(@board) if request.format.html?
+
respond_with_board
end
@@ -31,4 +35,18 @@ class Groups::BoardsController < Groups::ApplicationController
def serialize_as_json(resource)
resource.as_json(only: [:id])
end
+
+ def includes_board?(board_id)
+ boards.any? { |board| board.id == board_id }
+ end
+
+ def redirect_to_recent_board
+ return if request.format.json?
+
+ recently_visited = Boards::Visits::LatestService.new(group, current_user).execute
+
+ if recently_visited && includes_board?(recently_visited.board_id)
+ redirect_to(group_board_path(id: recently_visited.board_id), status: :found)
+ end
+ end
end
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/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index a7cee426cf1..b42116b0f36 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -10,7 +10,7 @@ class Groups::MilestonesController < Groups::ApplicationController
def index
respond_to do |format|
format.html do
- @milestone_states = GlobalMilestone.states_count(group_projects, group)
+ @milestone_states = Milestone.states_count(group_projects, [group])
@milestones = Kaminari.paginate_array(milestones).page(params[:page])
end
format.json do
diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb
index 93f3eb2be6d..c1dcc463de7 100644
--- a/app/controllers/groups/settings/ci_cd_controller.rb
+++ b/app/controllers/groups/settings/ci_cd_controller.rb
@@ -7,7 +7,7 @@ module Groups
before_action :authorize_admin_pipeline!
def show
- define_secret_variables
+ define_ci_variables
end
def reset_registration_token
@@ -19,7 +19,7 @@ module Groups
private
- def define_secret_variables
+ def define_ci_variables
@variable = Ci::GroupVariable.new(group: group)
.present(current_user: current_user)
@variables = group.variables.order_key_asc
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/boards_controller.rb b/app/controllers/projects/boards_controller.rb
index 77b818347c7..8189b5d182a 100644
--- a/app/controllers/projects/boards_controller.rb
+++ b/app/controllers/projects/boards_controller.rb
@@ -8,6 +8,7 @@ class Projects::BoardsController < Projects::ApplicationController
before_action :authorize_read_board!, only: [:index, :show]
before_action :boards, only: :index
before_action :assign_endpoint_vars
+ before_action :redirect_to_recent_board, only: :index
def index
respond_with_boards
@@ -16,6 +17,9 @@ class Projects::BoardsController < Projects::ApplicationController
def show
@board = boards.find(params[:id])
+ # add/update the board in the recent visited table
+ Boards::Visits::CreateService.new(@board.project, current_user).execute(@board) if request.format.html?
+
respond_with_board
end
@@ -33,10 +37,24 @@ class Projects::BoardsController < Projects::ApplicationController
end
def authorize_read_board!
- return access_denied! unless can?(current_user, :read_board, project)
+ access_denied! unless can?(current_user, :read_board, project)
end
def serialize_as_json(resource)
resource.as_json(only: [:id])
end
+
+ def includes_board?(board_id)
+ boards.any? { |board| board.id == board_id }
+ end
+
+ def redirect_to_recent_board
+ return if request.format.json?
+
+ recently_visited = Boards::Visits::LatestService.new(project, current_user).execute
+
+ if recently_visited && includes_board?(recently_visited.board_id)
+ redirect_to(namespace_project_board_path(id: recently_visited.board_id), status: :found)
+ end
+ end
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/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb
index 92ef10a9ef5..0a593bd35b6 100644
--- a/app/controllers/projects/deploy_keys_controller.rb
+++ b/app/controllers/projects/deploy_keys_controller.rb
@@ -46,7 +46,9 @@ class Projects::DeployKeysController < Projects::ApplicationController
end
def enable
- Projects::EnableDeployKeyService.new(@project, current_user, params).execute
+ key = Projects::EnableDeployKeyService.new(@project, current_user, params).execute
+
+ return render_404 unless key
respond_to do |format|
format.html { redirect_to_repository_settings(@project, anchor: 'js-deploy-keys-settings') }
@@ -54,19 +56,16 @@ class Projects::DeployKeysController < Projects::ApplicationController
end
end
- # rubocop: disable CodeReuse/ActiveRecord
def disable
- deploy_key_project = @project.deploy_keys_projects.find_by(deploy_key_id: params[:id])
- return render_404 unless deploy_key_project
+ deploy_key_project = Projects::DisableDeployKeyService.new(@project, current_user, params).execute
- deploy_key_project.destroy!
+ return render_404 unless deploy_key_project
respond_to do |format|
format.html { redirect_to_repository_settings(@project, anchor: 'js-deploy-keys-settings') }
format.json { head :ok }
end
end
- # rubocop: enable CodeReuse/ActiveRecord
protected
diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb
index be708835e30..c0aa39d87c6 100644
--- a/app/controllers/projects/git_http_controller.rb
+++ b/app/controllers/projects/git_http_controller.rb
@@ -8,6 +8,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
rescue_from Gitlab::GitAccess::UnauthorizedError, with: :render_403
rescue_from Gitlab::GitAccess::NotFoundError, with: :render_404
rescue_from Gitlab::GitAccess::ProjectCreationError, with: :render_422
+ rescue_from Gitlab::GitAccess::TimeoutError, with: :render_503
# GET /foo/bar.git/info/refs?service=git-upload-pack (git pull)
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
@@ -62,6 +63,10 @@ class Projects::GitHttpController < Projects::GitHttpClientController
render plain: exception.message, status: :unprocessable_entity
end
+ def render_503(exception)
+ render plain: exception.message, status: :service_unavailable
+ end
+
def access
@access ||= access_klass.new(access_actor, project,
'http', authentication_abilities: authentication_abilities,
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index b06a6f3bb0d..308f666394c 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -9,12 +9,25 @@ class Projects::IssuesController < Projects::ApplicationController
include IssuesCalendar
include SpammableActions
- prepend_before_action :authenticate_user!, only: [:new]
+ def self.authenticate_user_only_actions
+ %i[new]
+ end
+
+ def self.issue_except_actions
+ %i[index calendar new create bulk_update]
+ end
+
+ def self.set_issuables_index_only_actions
+ %i[index calendar]
+ end
+
+ prepend_before_action :authenticate_user!, only: authenticate_user_only_actions
before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update]
before_action :check_issues_available!
- before_action :issue, except: [:index, :calendar, :new, :create, :bulk_update]
- before_action :set_issuables_index, only: [:index, :calendar]
+ before_action :issue, except: issue_except_actions
+
+ before_action :set_issuables_index, only: set_issuables_index_only_actions
# Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create]
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 757b03d0b0e..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
@@ -168,7 +166,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
def merge
- return access_denied! unless @merge_request.can_be_merged_by?(current_user)
+ access_check_result = merge_access_check
+
+ return access_check_result if access_check_result
status = merge!
@@ -201,9 +201,11 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
def ci_environments_status
- environments = @merge_request.environments_for(current_user).map do |environment|
- EnvironmentStatus.new(environment, @merge_request)
- end
+ environments = if ci_environments_status_on_merge_result?
+ EnvironmentStatus.after_merge_request(@merge_request, current_user)
+ else
+ EnvironmentStatus.for_merge_request(@merge_request, current_user)
+ end
render json: EnvironmentStatusSerializer.new(current_user: current_user).represent(environments)
end
@@ -241,6 +243,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
private
+ def ci_environments_status_on_merge_result?
+ params[:environment_target] == 'merge_commit'
+ end
+
def target_branch_missing?
@merge_request.has_no_commits? && !@merge_request.target_branch_exists?
end
@@ -256,6 +262,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
return :failed
end
+ merge_service = ::MergeRequests::MergeService.new(@project, current_user, merge_params)
+
+ unless merge_service.hooks_validation_pass?(@merge_request)
+ return :hook_validation_error
+ end
+
return :sha_mismatch if params[:sha] != @merge_request.diff_head_sha
@merge_request.update(merge_error: nil, squash: merge_params.fetch(:squash, false))
@@ -318,6 +330,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
access_denied! unless access_check
end
+ def merge_access_check
+ access_denied! unless @merge_request.can_be_merged_by?(current_user)
+ end
+
def whitelist_query_limiting
# Also see https://gitlab.com/gitlab-org/gitlab-ce/issues/42441
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42438')
diff --git a/app/controllers/projects/mirrors_controller.rb b/app/controllers/projects/mirrors_controller.rb
index 78d5faf2326..53176978416 100644
--- a/app/controllers/projects/mirrors_controller.rb
+++ b/app/controllers/projects/mirrors_controller.rb
@@ -44,6 +44,22 @@ class Projects::MirrorsController < Projects::ApplicationController
redirect_to_repository_settings(project, anchor: 'js-push-remote-settings')
end
+ def ssh_host_keys
+ lookup = SshHostKey.new(project: project, url: params[:ssh_url], compare_host_keys: params[:compare_host_keys])
+
+ if lookup.error.present?
+ # Failed to read keys
+ render json: { message: lookup.error }, status: :bad_request
+ elsif lookup.known_hosts.nil?
+ # Still working, come back later
+ render body: nil, status: :no_content
+ else
+ render json: lookup
+ end
+ rescue ArgumentError => err
+ render json: { message: err.message }, status: :bad_request
+ end
+
private
def remote_mirror
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 3a1344651df..75e590f3f33 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -68,7 +68,7 @@ module Projects
def define_variables
define_runners_variables
- define_secret_variables
+ define_ci_variables
define_triggers_variables
define_badges_variables
define_auto_devops_variables
@@ -90,7 +90,7 @@ module Projects
@group_runners = ::Ci::Runner.belonging_to_parent_group_of_project(@project.id)
end
- def define_secret_variables
+ def define_ci_variables
@variable = ::Ci::Variable.new(project: project)
.present(current_user: current_user)
@variables = project.variables.order_key_asc
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/controllers/root_controller.rb b/app/controllers/root_controller.rb
index ebf70f25bda..7b6657e1196 100644
--- a/app/controllers/root_controller.rb
+++ b/app/controllers/root_controller.rb
@@ -45,9 +45,9 @@ class RootController < Dashboard::ProjectsController
when 'todos'
redirect_to(dashboard_todos_path)
when 'issues'
- redirect_to(issues_dashboard_path(assignee_id: current_user.id))
+ redirect_to(issues_dashboard_path(assignee_username: current_user.username))
when 'merge_requests'
- redirect_to(merge_requests_dashboard_path(assignee_id: current_user.id))
+ redirect_to(merge_requests_dashboard_path(assignee_username: current_user.username))
end
end
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/events_finder.rb b/app/finders/events_finder.rb
index 2e82bda8730..8df01f1dad9 100644
--- a/app/finders/events_finder.rb
+++ b/app/finders/events_finder.rb
@@ -58,7 +58,7 @@ class EventsFinder
def by_target_type(events)
return events unless Event::TARGET_TYPES[params[:target_type]]
- events.where(target_type: Event::TARGET_TYPES[params[:target_type]])
+ events.where(target_type: Event::TARGET_TYPES[params[:target_type]].name)
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/finders/group_descendants_finder.rb b/app/finders/group_descendants_finder.rb
index 9d57d2d3bc9..a9ce5be13f3 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
@@ -180,7 +178,7 @@ class GroupDescendantsFinder
end
def sort
- params.fetch(:sort, 'id_asc')
+ params.fetch(:sort, 'created_desc')
end
# rubocop: disable CodeReuse/ActiveRecord
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 8abfe0c4c17..fdc630cbf72 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -14,7 +14,9 @@
# project_id: integer
# milestone_title: string
# author_id: integer
-# assignee_id: integer
+# author_username: string
+# assignee_id: integer or 'None' or 'Any'
+# assignee_username: string
# search: string
# label_name: string
# sort: string
@@ -34,6 +36,11 @@ class IssuableFinder
requires_cross_project_access unless: -> { project? }
+ # This is used as a common filter for None / Any
+ FILTER_NONE = 'none'.freeze
+ FILTER_ANY = 'any'.freeze
+
+ # This is accepted as a deprecated filter and is also used in unassigning users
NONE = '0'.freeze
attr_accessor :current_user, :params
@@ -44,25 +51,15 @@ class IssuableFinder
assignee_username
author_id
author_username
- authorized_only
- group_id
- iids
label_name
milestone_title
my_reaction_emoji
- non_archived
- project_id
- scope
search
- sort
- state
- include_subgroups
- use_cte_for_search
]
end
def self.array_params
- @array_params ||= { label_name: [], iids: [], assignee_username: [] }
+ @array_params ||= { label_name: [], assignee_username: [] }
end
def self.valid_params
@@ -187,11 +184,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)
@@ -212,7 +204,6 @@ class IssuableFinder
Milestone.none
end
end
- # rubocop: enable CodeReuse/ActiveRecord
def labels?
params[:label_name].present?
@@ -222,7 +213,6 @@ class IssuableFinder
labels? && params[:label_name].include?(Label::None.title)
end
- # rubocop: disable CodeReuse/ActiveRecord
def labels
return @labels if defined?(@labels)
@@ -233,19 +223,13 @@ class IssuableFinder
Label.none
end
end
- # rubocop: enable CodeReuse/ActiveRecord
def assignee_id?
- params[:assignee_id].present? && params[:assignee_id].to_s != NONE
+ params[:assignee_id].present?
end
def assignee_username?
- params[:assignee_username].present? && params[:assignee_username].to_s != NONE
- end
-
- def no_assignee?
- # Assignee_id takes precedence over assignee_username
- params[:assignee_id].to_s == NONE || params[:assignee_username].to_s == NONE
+ params[:assignee_username].present?
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -399,18 +383,29 @@ class IssuableFinder
# rubocop: disable CodeReuse/ActiveRecord
def by_assignee(items)
- if assignee
- items = items.where(assignee_id: assignee.id)
- elsif no_assignee?
- items = items.where(assignee_id: nil)
+ if filter_by_no_assignee?
+ items.where(assignee_id: nil)
+ elsif filter_by_any_assignee?
+ items.where('assignee_id IS NOT NULL')
+ elsif assignee
+ items.where(assignee_id: assignee.id)
elsif assignee_id? || assignee_username? # assignee not found
- items = items.none
+ items.none
+ else
+ items
end
-
- items
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
@@ -425,18 +420,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?
@@ -458,6 +441,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?
@@ -473,12 +474,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 abdc47b9866..45e494725d7 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -135,17 +135,17 @@ class IssuesFinder < IssuableFinder
current_user.blank?
end
- # rubocop: disable CodeReuse/ActiveRecord
def by_assignee(items)
- if assignee
- items.assigned_to(assignee)
- elsif no_assignee?
+ if filter_by_no_assignee?
items.unassigned
+ elsif filter_by_any_assignee?
+ items.assigned
+ elsif assignee
+ items.assigned_to(assignee)
elsif assignee_id? || assignee_username? # assignee not found
items.none
else
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 5beea92689f..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, 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_helper.rb b/app/helpers/application_helper.rb
index 4f91e3e4117..74042f0bae8 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -173,17 +173,7 @@ module ApplicationHelper
without = options.delete(:without)
add_label = options.delete(:label)
- exist_opts = {
- state: params[:state],
- scope: params[:scope],
- milestone_title: params[:milestone_title],
- assignee_id: params[:assignee_id],
- author_id: params[:author_id],
- search: params[:search],
- label_name: params[:label_name]
- }
-
- options = exist_opts.merge(options)
+ options = request.query_parameters.merge(options)
if without.present?
without.each do |key|
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/auth_helper.rb b/app/helpers/auth_helper.rb
index c158cf20dd6..44f85e9c0f8 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -24,6 +24,23 @@ module AuthHelper
Gitlab::Auth::OAuth::Provider.label_for(name)
end
+ def form_based_provider_priority
+ ['crowd', /^ldap/, 'kerberos']
+ end
+
+ def form_based_provider_with_highest_priority
+ @form_based_provider_with_highest_priority ||= begin
+ form_based_provider_priority.each do |provider_regexp|
+ highest_priority = form_based_providers.find { |provider| provider.match?(provider_regexp) }
+ break highest_priority unless highest_priority.nil?
+ end
+ end
+ end
+
+ def form_based_auth_provider_has_active_class?(provider)
+ form_based_provider_with_highest_priority == provider
+ end
+
def form_based_provider?(name)
[LDAP_PROVIDER, 'crowd'].any? { |pattern| pattern === name.to_s }
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/dashboard_helper.rb b/app/helpers/dashboard_helper.rb
index 463f4145bdd..d90ef8903a7 100644
--- a/app/helpers/dashboard_helper.rb
+++ b/app/helpers/dashboard_helper.rb
@@ -2,11 +2,11 @@
module DashboardHelper
def assigned_issues_dashboard_path
- issues_dashboard_path(assignee_id: current_user.id)
+ issues_dashboard_path(assignee_username: current_user.username)
end
def assigned_mrs_dashboard_path
- merge_requests_dashboard_path(assignee_id: current_user.id)
+ merge_requests_dashboard_path(assignee_username: current_user.username)
end
def dashboard_nav_links
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/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/search_helper.rb b/app/helpers/search_helper.rb
index 4f9e1322b56..80cc568820a 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -163,15 +163,26 @@ module SearchHelper
if @project.present?
opts[:data]['project-id'] = @project.id
opts[:data]['base-endpoint'] = project_path(@project)
- else
- # Group context
+ elsif @group.present?
opts[:data]['group-id'] = @group.id
opts[:data]['base-endpoint'] = group_canonical_path(@group)
+ else
+ opts[:data]['base-endpoint'] = root_dashboard_path
end
opts
end
+ def search_history_storage_prefix
+ if @project.present?
+ @project.full_path
+ elsif @group.present?
+ @group.full_path
+ else
+ 'dashboard'
+ end
+ end
+
# Sanitize a HTML field for search display. Most tags are stripped out and the
# maximum length is set to 200 characters.
def search_md_sanitize(object, field)
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/board_group_recent_visit.rb b/app/models/board_group_recent_visit.rb
new file mode 100644
index 00000000000..92abbb67222
--- /dev/null
+++ b/app/models/board_group_recent_visit.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+# Tracks which boards in a specific group a user has visited
+class BoardGroupRecentVisit < ActiveRecord::Base
+ belongs_to :user
+ belongs_to :group
+ belongs_to :board
+
+ validates :user, presence: true
+ validates :group, presence: true
+ validates :board, presence: true
+
+ scope :by_user_group, -> (user, group) { where(user: user, group: group).order(:updated_at) }
+
+ def self.visited!(user, board)
+ visit = find_or_create_by(user: user, group: board.group, board: board)
+ visit.touch if visit.updated_at < Time.now
+ rescue ActiveRecord::RecordNotUnique
+ retry
+ end
+
+ def self.latest(user, group)
+ by_user_group(user, group).last
+ end
+end
diff --git a/app/models/board_project_recent_visit.rb b/app/models/board_project_recent_visit.rb
new file mode 100644
index 00000000000..7cffff906d8
--- /dev/null
+++ b/app/models/board_project_recent_visit.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+# Tracks which boards in a specific project a user has visited
+class BoardProjectRecentVisit < ActiveRecord::Base
+ belongs_to :user
+ belongs_to :project
+ belongs_to :board
+
+ validates :user, presence: true
+ validates :project, presence: true
+ validates :board, presence: true
+
+ scope :by_user_project, -> (user, project) { where(user: user, project: project).order(:updated_at) }
+
+ def self.visited!(user, board)
+ visit = find_or_create_by(user: user, project: board.project, board: board)
+ visit.touch if visit.updated_at < Time.now
+ rescue ActiveRecord::RecordNotUnique
+ retry
+ end
+
+ def self.latest(user, project)
+ by_user_project(user, project).last
+ end
+end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index cdfe8175a42..d60861dc95f 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
@@ -99,7 +98,7 @@ module Ci
scope :matches_tag_ids, -> (tag_ids) do
matcher = ::ActsAsTaggableOn::Tagging
- .where(taggable_type: CommitStatus)
+ .where(taggable_type: CommitStatus.name)
.where(context: 'tags')
.where('taggable_id = ci_builds.id')
.where.not(tag_id: tag_ids).select('1')
@@ -109,7 +108,7 @@ module Ci
scope :with_any_tags, -> do
matcher = ::ActsAsTaggableOn::Tagging
- .where(taggable_type: CommitStatus)
+ .where(taggable_type: CommitStatus.name)
.where(context: 'tags')
.where('taggable_id = ci_builds.id').select('1')
@@ -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:
#
@@ -419,7 +464,9 @@ module Ci
end
def repo_url
- auth = "gitlab-ci-token:#{ensure_token!}@"
+ return unless token
+
+ auth = "gitlab-ci-token:#{token}@"
project.http_url_to_repo.sub(%r{^https?://}) do |prefix|
prefix + auth
end
@@ -593,11 +640,11 @@ module Ci
def secret_group_variables
return [] unless project.group
- project.group.secret_variables_for(ref, project)
+ project.group.ci_variables_for(ref, project)
end
def secret_project_variables(environment: persisted_environment)
- project.secret_variables_for(ref: ref, environment: environment)
+ project.ci_variables_for(ref: ref, environment: environment)
end
def steps
@@ -680,7 +727,7 @@ module Ci
trace = trace.dup
Gitlab::Ci::MaskSecret.mask!(trace, project.runners_token) if project
- Gitlab::Ci::MaskSecret.mask!(trace, token)
+ Gitlab::Ci::MaskSecret.mask!(trace, token) if token
trace
end
@@ -706,7 +753,7 @@ module Ci
if success?
return successful_deployment_status
- elsif complete? && !success?
+ elsif failed?
return :failed
end
@@ -723,13 +770,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)
@@ -771,26 +816,26 @@ module Ci
.concat(pipeline.persisted_variables)
.append(key: 'CI_JOB_ID', value: id.to_s)
.append(key: 'CI_JOB_URL', value: Gitlab::Routing.url_helpers.project_job_url(project, self))
- .append(key: 'CI_JOB_TOKEN', value: token, public: false)
+ .append(key: 'CI_JOB_TOKEN', value: token.to_s, public: false)
.append(key: 'CI_BUILD_ID', value: id.to_s)
- .append(key: 'CI_BUILD_TOKEN', value: token, public: false)
+ .append(key: 'CI_BUILD_TOKEN', value: token.to_s, public: false)
.append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER)
- .append(key: 'CI_REGISTRY_PASSWORD', value: token, public: false)
- .append(key: 'CI_REPOSITORY_URL', value: repo_url, public: false)
+ .append(key: 'CI_REGISTRY_PASSWORD', value: token.to_s, public: false)
+ .append(key: 'CI_REPOSITORY_URL', value: repo_url.to_s, public: false)
.concat(deploy_token_variables)
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')
variables.append(key: 'GITLAB_FEATURES', value: project.licensed_features.join(','))
variables.append(key: 'CI_SERVER_NAME', value: 'GitLab')
variables.append(key: 'CI_SERVER_VERSION', value: Gitlab::VERSION)
- variables.append(key: 'CI_SERVER_VERSION_MAJOR', value: gitlab_version_info.major.to_s)
- variables.append(key: 'CI_SERVER_VERSION_MINOR', value: gitlab_version_info.minor.to_s)
- variables.append(key: 'CI_SERVER_VERSION_PATCH', value: gitlab_version_info.patch.to_s)
+ variables.append(key: 'CI_SERVER_VERSION_MAJOR', value: Gitlab.version_info.major.to_s)
+ variables.append(key: 'CI_SERVER_VERSION_MINOR', value: Gitlab.version_info.minor.to_s)
+ variables.append(key: 'CI_SERVER_VERSION_PATCH', value: Gitlab.version_info.patch.to_s)
variables.append(key: 'CI_SERVER_REVISION', value: Gitlab.revision)
variables.append(key: 'CI_JOB_NAME', value: name)
variables.append(key: 'CI_JOB_STAGE', value: stage)
@@ -801,14 +846,12 @@ 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
- def gitlab_version_info
- @gitlab_version_info ||= Gitlab::VersionInfo.parse(Gitlab::VERSION)
- end
-
def legacy_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI_BUILD_REF', value: sha)
@@ -849,6 +892,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..9512ba42f67 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -58,15 +58,9 @@ module Ci
after_create :keep_around_commits, unless: :importing?
- enum_with_nil source: {
- unknown: nil,
- push: 1,
- web: 2,
- trigger: 3,
- schedule: 4,
- api: 5,
- external: 6
- }
+ # We use `Ci::PipelineEnums.sources` here so that EE can more easily extend
+ # this `Hash` with new values.
+ enum_with_nil source: ::Ci::PipelineEnums.sources
enum_with_nil config_source: {
unknown_source: nil,
@@ -74,10 +68,9 @@ module Ci
auto_devops_source: 2
}
- enum failure_reason: {
- unknown_failure: 0,
- config_error: 1
- }
+ # We use `Ci::PipelineEnums.failure_reasons` here so that EE can more easily
+ # extend this `Hash` with new values.
+ enum failure_reason: ::Ci::PipelineEnums.failure_reasons
state_machine :status, initial: :created do
event :enqueue do
@@ -181,22 +174,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
- ref ? relation.where(ref: ref) : relation
+ 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
+
+ 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 +240,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/ci/pipeline_enums.rb b/app/models/ci/pipeline_enums.rb
new file mode 100644
index 00000000000..8d8d16e2ec1
--- /dev/null
+++ b/app/models/ci/pipeline_enums.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Ci
+ module PipelineEnums
+ # Returns the `Hash` to use for creating the `failure_reason` enum for
+ # `Ci::Pipeline`.
+ def self.failure_reasons
+ {
+ unknown_failure: 0,
+ config_error: 1
+ }
+ end
+
+ # Returns the `Hash` to use for creating the `sources` enum for
+ # `Ci::Pipeline`.
+ def self.sources
+ {
+ unknown: nil,
+ push: 1,
+ web: 2,
+ trigger: 3,
+ schedule: 4,
+ api: 5,
+ external: 6
+ }
+ end
+ end
+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 95efecfc41d..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,17 +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'
# 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'
@@ -38,8 +45,12 @@ module Clusters
accepts_nested_attributes_for :platform_kubernetes, update_only: true
validates :name, cluster_name: true
+ validates :cluster_type, presence: true
validate :restrict_modification, on: :update
+ validate :no_groups, unless: :group_type?
+ validate :no_projects, unless: :project_type?
+
delegate :status, to: :provider, allow_nil: true
delegate :status_reason, to: :provider, allow_nil: true
delegate :on_creation?, to: :provider, allow_nil: true
@@ -50,6 +61,12 @@ module Clusters
delegate :available?, to: :application_ingress, prefix: true, allow_nil: true
delegate :available?, to: :application_prometheus, prefix: true, allow_nil: true
+ enum cluster_type: {
+ instance_type: 1,
+ group_type: 2,
+ project_type: 3
+ }
+
enum platform_type: {
kubernetes: 1
}
@@ -85,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
@@ -102,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
@@ -122,5 +158,17 @@ module Clusters
true
end
+
+ def no_groups
+ if groups.any?
+ errors.add(:cluster, 'cannot have groups assigned')
+ end
+ end
+
+ def no_projects
+ if projects.any?
+ errors.add(:cluster, 'cannot have projects assigned')
+ end
+ end
end
end
diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb
index 93bdf9c223d..0e74cce29b7 100644
--- a/app/models/clusters/concerns/application_status.rb
+++ b/app/models/clusters/concerns/application_status.rb
@@ -64,6 +64,13 @@ module Clusters
status_reason = transition.args.first
app_status.status_reason = status_reason if status_reason
end
+
+ before_transition any => [:installed, :updated] do |app_status, _|
+ # When installing any application we are also performing an update
+ # of tiller (see Gitlab::Kubernetes::Helm::ClientCommand) so
+ # therefore we need to reflect that in the database.
+ app_status.cluster.application_helm.update!(version: Gitlab::Kubernetes::Helm::HELM_VERSION)
+ end
end
end
diff --git a/app/models/clusters/group.rb b/app/models/clusters/group.rb
new file mode 100644
index 00000000000..2b08a9e47f0
--- /dev/null
+++ b/app/models/clusters/group.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module Clusters
+ class Group < ActiveRecord::Base
+ self.table_name = 'cluster_groups'
+
+ belongs_to :cluster, class_name: 'Clusters::Cluster'
+ belongs_to :group, class_name: '::Group'
+ end
+end
diff --git a/app/models/clusters/kubernetes_namespace.rb b/app/models/clusters/kubernetes_namespace.rb
index fb5f6b65d9d..34f5e38ff79 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'
@@ -9,41 +11,73 @@ module Clusters
belongs_to :project, class_name: '::Project'
has_one :platform_kubernetes, through: :cluster
+ before_validation :set_defaults
+
validates :namespace, presence: true
validates :namespace, uniqueness: { scope: :cluster_id }
- before_validation :set_namespace_and_service_account_to_default, on: :create
+ validates :service_account_name, presence: true
+
+ 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 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.to_s)
+ .append(key: 'KUBE_NAMESPACE', value: namespace.to_s)
+ .append(key: 'KUBE_TOKEN', value: service_account_token.to_s, public: false)
+ .append(key: 'KUBECONFIG', value: config, public: false, file: true)
+ end
end
- def default_namespace
- platform_kubernetes&.namespace.presence || project_namespace
+ def set_defaults
+ self.namespace ||= default_platform_kubernetes_namespace
+ self.namespace ||= default_project_namespace
+ self.service_account_name ||= default_service_account_name
end
+ private
+
def default_service_account_name
+ return unless namespace
+
"#{namespace}-service-account"
end
- def project_namespace
- Gitlab::NamespaceSanitizer.sanitize(project_slug)
+ def default_platform_kubernetes_namespace
+ platform_kubernetes&.namespace.presence
+ end
+
+ def default_project_namespace
+ Gitlab::NamespaceSanitizer.sanitize(project_slug) if project_slug
end
def project_slug
+ return unless project
+
"#{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 e8e943872de..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
@@ -107,7 +123,7 @@ module Clusters
end
def kubeclient
- @kubeclient ||= build_kube_client!(api_groups: ['api', 'apis/rbac.authorization.k8s.io'])
+ @kubeclient ||= build_kube_client!
end
private
@@ -136,8 +152,9 @@ module Clusters
Gitlab::NamespaceSanitizer.sanitize(slug)
end
- def build_kube_client!(api_groups: ['api'], api_version: 'v1')
- raise "Incomplete settings" unless api_url && actual_namespace
+ def build_kube_client!
+ 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"
@@ -145,8 +162,6 @@ module Clusters
Gitlab::Kubernetes::KubeClient.new(
api_url,
- api_groups,
- api_version,
auth_options: kubeclient_auth_options,
ssl_options: kubeclient_ssl_options,
http_proxy_uri: ENV['http_proxy']
@@ -158,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
@@ -191,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?
@@ -201,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..546fcc54a15 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -230,24 +230,13 @@ class Commit
def lazy_author
BatchLoader.for(author_email.downcase).batch do |emails, loader|
- # A Hash that maps user Emails to the corresponding User objects. The
- # Emails at this point are the _primary_ Emails of the Users.
- users_for_emails = User
- .by_any_email(emails)
- .each_with_object({}) { |user, hash| hash[user.email] = user }
-
- users_for_ids = users_for_emails
- .values
- .each_with_object({}) { |user, hash| hash[user.id] = user }
-
- # Some commits may have used an alternative Email address. In this case we
- # need to query the "emails" table to map those addresses to User objects.
- Email
- .where(email: emails - users_for_emails.keys)
- .pluck(:email, :user_id)
- .each { |(email, id)| users_for_emails[email] = users_for_ids[id] }
-
- users_for_emails.each { |email, user| loader.call(email, user) }
+ users = User.by_any_email(emails).includes(:emails)
+
+ emails.each do |email|
+ user = users.find { |u| u.any_email?(email) }
+
+ loader.call(email, user)
+ end
end
end
@@ -260,7 +249,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 06507345fe8..0f50bd39131 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -42,16 +42,9 @@ class CommitStatus < ActiveRecord::Base
scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) }
scope :after_stage, -> (index) { where('stage_idx > ?', index) }
- enum_with_nil failure_reason: {
- unknown_failure: nil,
- script_failure: 1,
- api_failure: 2,
- stuck_or_timeout_failure: 3,
- runner_system_failure: 4,
- missing_dependency_failure: 5,
- runner_unsupported: 6,
- stale_schedule: 7
- }
+ # We use `CommitStatusEnums.failure_reasons` here so that EE can more easily
+ # extend this `Hash` with new values.
+ enum_with_nil failure_reason: ::CommitStatusEnums.failure_reasons
##
# We still create some CommitStatuses outside of CreatePipelineService.
@@ -109,7 +102,7 @@ class CommitStatus < ActiveRecord::Base
before_transition any => :failed do |commit_status, transition|
failure_reason = transition.args.first
- commit_status.failure_reason = failure_reason
+ commit_status.failure_reason = CommitStatus.failure_reasons[failure_reason]
end
after_transition do |commit_status, transition|
@@ -166,16 +159,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/commit_status_enums.rb b/app/models/commit_status_enums.rb
new file mode 100644
index 00000000000..152105d9429
--- /dev/null
+++ b/app/models/commit_status_enums.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module CommitStatusEnums
+ # Returns the Hash to use for creating the `failure_reason` enum for
+ # `CommitStatus`.
+ def self.failure_reasons
+ {
+ unknown_failure: nil,
+ script_failure: 1,
+ api_failure: 2,
+ stuck_or_timeout_failure: 3,
+ runner_system_failure: 4,
+ missing_dependency_failure: 5,
+ runner_unsupported: 6,
+ stale_schedule: 7,
+ job_execution_timeout: 8,
+ archived_failure: 9
+ }
+ end
+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/avatarable.rb b/app/models/concerns/avatarable.rb
index 0d5311a9985..b42236c1fa2 100644
--- a/app/models/concerns/avatarable.rb
+++ b/app/models/concerns/avatarable.rb
@@ -86,7 +86,7 @@ module Avatarable
params[:model].upload_paths(params[:identifier])
end
- Upload.where(uploader: AvatarUploader, path: paths).find_each do |upload|
+ Upload.where(uploader: AvatarUploader.name, path: paths).find_each do |upload|
model = model_class.instantiate('id' => upload.model_id)
loader.call({ model: model, identifier: File.basename(upload.path) }, upload)
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/deployment_platform.rb b/app/models/concerns/deployment_platform.rb
index 91052013592..e57a3383544 100644
--- a/app/models/concerns/deployment_platform.rb
+++ b/app/models/concerns/deployment_platform.rb
@@ -42,6 +42,7 @@ module DeploymentPlatform
{
name: 'kubernetes-template',
projects: [self],
+ cluster_type: :project_type,
provider_type: :user,
platform_type: :kubernetes,
platform_kubernetes_attributes: platform_kubernetes_attributes_from_service_template
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 2aa52bbaeea..69c5affe142 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -9,6 +9,7 @@
module Issuable
extend ActiveSupport::Concern
include Gitlab::SQL::Pattern
+ include Redactable
include CacheMarkdownField
include Participable
include Mentionable
@@ -32,6 +33,8 @@ module Issuable
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description, issuable_state_filter_enabled: true
+ redact_field :description
+
belongs_to :author, class_name: "User"
belongs_to :updated_by, class_name: "User"
belongs_to :last_edited_by, class_name: 'User'
@@ -360,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/redactable.rb b/app/models/concerns/redactable.rb
new file mode 100644
index 00000000000..5ad96d6cc46
--- /dev/null
+++ b/app/models/concerns/redactable.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+# This module searches and redacts sensitive information in
+# redactable fields. Currently only unsubscribe link is redacted.
+# Add following lines into your model:
+#
+# include Redactable
+# redact_field :foo
+#
+module Redactable
+ extend ActiveSupport::Concern
+
+ UNSUBSCRIBE_PATTERN = %r{/sent_notifications/\h{32}/unsubscribe}
+
+ class_methods do
+ def redact_field(field)
+ before_validation do
+ redact_field!(field) if attribute_changed?(field)
+ end
+ end
+ end
+
+ private
+
+ def redact_field!(field)
+ text = public_send(field) # rubocop:disable GitlabSecurity/PublicSend
+ return unless text.present?
+
+ redacted = text.gsub(UNSUBSCRIBE_PATTERN, '/sent_notifications/REDACTED/unsubscribe')
+
+ public_send("#{field}=", redacted) # rubocop:disable GitlabSecurity/PublicSend
+ end
+end
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 522b65e4205..23a43aec677 100644
--- a/app/models/concerns/token_authenticatable.rb
+++ b/app/models/concerns/token_authenticatable.rb
@@ -5,57 +5,53 @@ module TokenAuthenticatable
private
- def write_new_token(token_field)
- new_token = generate_available_token(token_field)
- write_attribute(token_field, new_token)
- end
-
- def generate_available_token(token_field)
- loop do
- token = generate_token(token_field)
- break token unless self.class.unscoped.find_by(token_field => token)
- end
- end
-
- def generate_token(token_field)
- Devise.friendly_token
- end
-
class_methods do
- def authentication_token_fields
- @token_fields || []
- end
-
private # rubocop:disable Lint/UselessAccessModifier
- def add_authentication_token_field(token_field)
+ 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")
+ end
+
@token_fields << token_field
- define_singleton_method("find_by_#{token_field}") do |token|
- find_by(token_field => token) if token
+ attr_accessor :cleartext_tokens
+
+ strategy = if options[:digest]
+ TokenAuthenticatableStrategies::Digest.new(self, token_field, options)
+ else
+ TokenAuthenticatableStrategies::Insecure.new(self, token_field, options)
+ end
+
+ if unique
+ define_singleton_method("find_by_#{token_field}") do |token|
+ strategy.find_token_authenticatable(token)
+ end
end
- define_method("ensure_#{token_field}") do
- current_token = read_attribute(token_field)
- current_token.blank? ? write_new_token(token_field) : current_token
+ define_method(token_field) do
+ strategy.get_token(self)
end
define_method("set_#{token_field}") do |token|
- write_attribute(token_field, token) if token
+ strategy.set_token(self, token)
+ end
+
+ define_method("ensure_#{token_field}") do
+ strategy.ensure_token(self)
end
# Returns a token, but only saves when the database is in read & write mode
define_method("ensure_#{token_field}!") do
- send("reset_#{token_field}!") if read_attribute(token_field).blank? # rubocop:disable GitlabSecurity/PublicSend
-
- read_attribute(token_field)
+ strategy.ensure_token!(self)
end
# Resets the token, but only saves when the database is in read & write mode
define_method("reset_#{token_field}!") do
- write_new_token(token_field)
- save! if Gitlab::Database.read_write?
+ strategy.reset_token!(self)
end
end
end
diff --git a/app/models/concerns/token_authenticatable_strategies/base.rb b/app/models/concerns/token_authenticatable_strategies/base.rb
new file mode 100644
index 00000000000..413721d3e6c
--- /dev/null
+++ b/app/models/concerns/token_authenticatable_strategies/base.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+module TokenAuthenticatableStrategies
+ class Base
+ def initialize(klass, token_field, options)
+ @klass = klass
+ @token_field = token_field
+ @options = options
+ end
+
+ def find_token_authenticatable(instance, unscoped = false)
+ raise NotImplementedError
+ end
+
+ def get_token(instance)
+ raise NotImplementedError
+ end
+
+ def set_token(instance)
+ raise NotImplementedError
+ end
+
+ def ensure_token(instance)
+ write_new_token(instance) unless token_set?(instance)
+ end
+
+ # Returns a token, but only saves when the database is in read & write mode
+ def ensure_token!(instance)
+ reset_token!(instance) unless token_set?(instance)
+ get_token(instance)
+ end
+
+ # Resets the token, but only saves when the database is in read & write mode
+ def reset_token!(instance)
+ write_new_token(instance)
+ instance.save! if Gitlab::Database.read_write?
+ end
+
+ protected
+
+ def write_new_token(instance)
+ new_token = generate_available_token
+ 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 unique && find_token_authenticatable(token, true)
+ end
+ end
+
+ def generate_token
+ @options[:token_generator] ? @options[:token_generator].call : Devise.friendly_token
+ end
+
+ def relation(unscoped)
+ unscoped ? @klass.unscoped : @klass
+ end
+
+ def token_set?(instance)
+ raise NotImplementedError
+ end
+
+ def token_field_name
+ @token_field
+ end
+ end
+end
diff --git a/app/models/concerns/token_authenticatable_strategies/digest.rb b/app/models/concerns/token_authenticatable_strategies/digest.rb
new file mode 100644
index 00000000000..9926662ed66
--- /dev/null
+++ b/app/models/concerns/token_authenticatable_strategies/digest.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module TokenAuthenticatableStrategies
+ class Digest < Base
+ def find_token_authenticatable(token, unscoped = false)
+ return unless token
+
+ token_authenticatable = relation(unscoped).find_by(token_field_name => Gitlab::CryptoHelper.sha256(token))
+
+ if @options[:fallback]
+ token_authenticatable ||= fallback_strategy.find_token_authenticatable(token)
+ end
+
+ token_authenticatable
+ end
+
+ def get_token(instance)
+ token = instance.cleartext_tokens&.[](@token_field)
+ token ||= fallback_strategy.get_token(instance) if @options[:fallback]
+
+ token
+ end
+
+ def set_token(instance, token)
+ return unless token
+
+ instance.cleartext_tokens ||= {}
+ instance.cleartext_tokens[@token_field] = token
+ instance[token_field_name] = Gitlab::CryptoHelper.sha256(token)
+ instance[@token_field] = nil if @options[:fallback]
+ end
+
+ protected
+
+ def fallback_strategy
+ @fallback_strategy ||= TokenAuthenticatableStrategies::Insecure.new(@klass, @token_field, @options)
+ end
+
+ def token_set?(instance)
+ token_digest = instance.read_attribute(token_field_name)
+ token_digest ||= instance.read_attribute(@token_field) if @options[:fallback]
+
+ token_digest.present?
+ end
+
+ def token_field_name
+ "#{@token_field}_digest"
+ end
+ end
+end
diff --git a/app/models/concerns/token_authenticatable_strategies/insecure.rb b/app/models/concerns/token_authenticatable_strategies/insecure.rb
new file mode 100644
index 00000000000..5f915259521
--- /dev/null
+++ b/app/models/concerns/token_authenticatable_strategies/insecure.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module TokenAuthenticatableStrategies
+ class Insecure < Base
+ def find_token_authenticatable(token, unscoped = false)
+ relation(unscoped).find_by(@token_field => token) if token
+ end
+
+ def get_token(instance)
+ instance.read_attribute(@token_field)
+ end
+
+ def set_token(instance, token)
+ instance[@token_field] = token if token
+ end
+
+ protected
+
+ def token_set?(instance)
+ instance.read_attribute(@token_field).present?
+ end
+ 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 62dc0f2cbeb..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,26 +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) || {}
+ metrics&.merge(deployment_time: finished_at.to_i) || {}
end
private
@@ -136,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 0816c395185..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
@@ -149,7 +150,7 @@ class Environment < ActiveRecord::Base
end
def has_metrics?
- prometheus_adapter&.can_query? && available? && last_deployment.present?
+ prometheus_adapter&.can_query? && available?
end
def metrics
diff --git a/app/models/environment_status.rb b/app/models/environment_status.rb
index 5ff3acc0e58..7078496ff52 100644
--- a/app/models/environment_status.rb
+++ b/app/models/environment_status.rb
@@ -3,33 +3,39 @@
class EnvironmentStatus
include Gitlab::Utils::StrongMemoize
- attr_reader :environment, :merge_request
+ attr_reader :environment, :merge_request, :sha
delegate :id, to: :environment
delegate :name, to: :environment
delegate :project, to: :environment
delegate :deployed_at, to: :deployment, allow_nil: true
- def initialize(environment, merge_request)
+ def self.for_merge_request(mr, user)
+ 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_commit_sha)
+ end
+
+ def initialize(environment, merge_request, sha)
@environment = environment
@merge_request = merge_request
+ @sha = sha
end
def deployment
strong_memoize(:deployment) do
- environment.first_deployment_for(merge_request.diff_head_sha)
+ Deployment.where(environment: environment).find_by_sha(sha)
end
end
- def deployed_at
- deployment&.created_at
- end
-
def changes
- sha = merge_request.diff_head_sha
return [] if project.route_map_for(sha).nil?
- changed_files.map { |file| build_change(file, sha) }.compact
+ changed_files.map { |file| build_change(file) }.compact
end
def changed_files
@@ -37,11 +43,27 @@ 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
- def build_change(file, sha)
+ def build_change(file)
public_path = project.public_path_for_source_path(file.new_path, sha)
return if public_path.nil?
@@ -53,4 +75,15 @@ class EnvironmentStatus
external_url: environment.external_url_for(file.new_path, sha)
}
end
+
+ 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)
+
+ EnvironmentStatus.new(environment, mr, sha)
+ end.compact
+ end
+ private_class_method :build_environments_status
end
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index a6cebabe089..085ffd16c6a 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -34,50 +34,6 @@ class GlobalMilestone
new(title, child_milestones)
end
- def self.states_count(projects, group = nil)
- legacy_group_milestones_count = legacy_group_milestone_states_count(projects)
- group_milestones_count = group_milestones_states_count(group)
-
- legacy_group_milestones_count.merge(group_milestones_count) do |k, legacy_group_milestones_count, group_milestones_count|
- legacy_group_milestones_count + group_milestones_count
- end
- end
-
- def self.group_milestones_states_count(group)
- return STATE_COUNT_HASH unless group
-
- params = { group_ids: [group.id], state: 'all' }
-
- relation = MilestonesFinder.new(params).execute # rubocop: disable CodeReuse/Finder
- grouped_by_state = relation.reorder(nil).group(:state).count
-
- {
- opened: grouped_by_state['active'] || 0,
- closed: grouped_by_state['closed'] || 0,
- all: grouped_by_state.values.sum
- }
- end
-
- # Counts the legacy group milestones which must be grouped by title
- def self.legacy_group_milestone_states_count(projects)
- return STATE_COUNT_HASH unless projects
-
- params = { project_ids: projects.map(&:id), state: 'all' }
-
- relation = MilestonesFinder.new(params).execute # rubocop: disable CodeReuse/Finder
- project_milestones_by_state_and_title = relation.reorder(nil).group(:state, :title).count
-
- opened = count_by_state(project_milestones_by_state_and_title, 'active')
- closed = count_by_state(project_milestones_by_state_and_title, 'closed')
- all = project_milestones_by_state_and_title.map { |(_, title), _| title }.uniq.count
-
- {
- opened: opened,
- closed: closed,
- all: all
- }
- end
-
def self.count_by_state(milestones_by_state_and_title, state)
milestones_by_state_and_title.count do |(milestone_state, _), _|
milestone_state == state
diff --git a/app/models/group.rb b/app/models/group.rb
index 612c546ca57..adb9169cfcd 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -41,6 +41,9 @@ class Group < Namespace
has_many :boards
has_many :badges, class_name: 'GroupBadge'
+ has_many :cluster_groups, class_name: 'Clusters::Group'
+ has_many :clusters, through: :cluster_groups, class_name: 'Clusters::Cluster'
+
has_many :todos
accepts_nested_attributes_for :variables, allow_destroy: true
@@ -366,7 +369,7 @@ class Group < Namespace
}
end
- def secret_variables_for(ref, project)
+ def ci_variables_for(ref, project)
list_of_ids = [self] + ancestors
variables = Ci::GroupVariable.where(group: list_of_ids)
variables = variables.unprotected unless project.protected_for?(ref)
diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb
index 7d9f6d89d44..8f305dd7c22 100644
--- a/app/models/hooks/service_hook.rb
+++ b/app/models/hooks/service_hook.rb
@@ -5,8 +5,8 @@ class ServiceHook < WebHook
validates :service, presence: true
# rubocop: disable CodeReuse/ServiceClass
- def execute(data)
- WebHookService.new(self, data, 'service_hook').execute
+ def execute(data, hook_name = 'service_hook')
+ WebHookService.new(self, data, hook_name).execute
end
# rubocop: enable CodeReuse/ServiceClass
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/lfs_object.rb b/app/models/lfs_object.rb
index 97bf5d611c2..69c563545bb 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -7,7 +7,7 @@ class LfsObject < ActiveRecord::Base
has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, through: :lfs_objects_projects
- scope :with_files_stored_locally, -> { where(file_store: [nil, LfsObjectUploader::Store::LOCAL]) }
+ scope :with_files_stored_locally, -> { where(file_store: LfsObjectUploader::Store::LOCAL) }
validates :oid, presence: true, uniqueness: true
@@ -26,7 +26,7 @@ class LfsObject < ActiveRecord::Base
end
def local_store?
- [nil, LfsObjectUploader::Store::LOCAL].include?(self.file_store)
+ file_store == LfsObjectUploader::Store::LOCAL
end
# rubocop: disable DestroyAll
diff --git a/app/models/member.rb b/app/models/member.rb
index 0696ea46c8b..bc8ac14d148 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -152,11 +152,13 @@ class Member < ActiveRecord::Base
return member unless can_update_member?(current_user, member)
- member.attributes = {
- created_by: member.created_by || current_user,
- access_level: access_level,
- expires_at: expires_at
- }
+ set_member_attributes(
+ member,
+ access_level,
+ current_user: current_user,
+ expires_at: expires_at,
+ ldap: ldap
+ )
if member.request?
::Members::ApproveAccessRequestService.new(
@@ -175,6 +177,18 @@ class Member < ActiveRecord::Base
# rubocop: enable CodeReuse/ServiceClass
end
+ # Populates the attributes of a member.
+ #
+ # This logic resides in a separate method so that EE can extend this logic,
+ # without having to patch the `add_user` method directly.
+ def set_member_attributes(member, access_level, current_user: nil, expires_at: nil, ldap: false)
+ member.attributes = {
+ created_by: member.created_by || current_user,
+ access_level: access_level,
+ expires_at: expires_at
+ }
+ end
+
def add_users(source, users, access_level, current_user: nil, expires_at: nil)
return [] unless users.present?
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 6559f94a696..92add079a02 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -204,6 +204,12 @@ class MergeRequest < ActiveRecord::Base
head_pipeline&.sha == diff_head_sha ? head_pipeline : nil
end
+ def merge_pipeline
+ return unless merged?
+
+ target_project.pipeline_for(target_branch, merge_commit_sha)
+ end
+
# Pattern used to extract `!123` merge request references from text
#
# This pattern supports cross-project references.
@@ -347,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.
@@ -394,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
@@ -939,7 +966,6 @@ class MergeRequest < ActiveRecord::Base
def mergeable_ci_state?
return true unless project.only_allow_merge_if_pipeline_succeeds?
- return true unless head_pipeline
actual_head_pipeline&.success? || actual_head_pipeline&.skipped?
end
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/milestone.rb b/app/models/milestone.rb
index 892a680f221..3cc8e2c44bb 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -145,7 +145,7 @@ class Milestone < ActiveRecord::Base
end
def participants
- User.joins(assigned_issues: :milestone).where("milestones.id = ?", id).uniq
+ User.joins(assigned_issues: :milestone).where("milestones.id = ?", id).distinct
end
def self.sort_by_attribute(method)
@@ -170,6 +170,22 @@ class Milestone < ActiveRecord::Base
sorted.with_order_id_desc
end
+ def self.states_count(projects, groups = nil)
+ return STATE_COUNT_HASH unless projects || groups
+
+ counts = Milestone
+ .for_projects_and_groups(projects&.map(&:id), groups&.map(&:id))
+ .reorder(nil)
+ .group(:state)
+ .count
+
+ {
+ opened: counts['active'] || 0,
+ closed: counts['closed'] || 0,
+ all: counts.values.sum
+ }
+ end
+
##
# Returns the String necessary to reference this Milestone in Markdown. Group
# milestones only support name references, and do not support cross-project
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 e1bd943e8e4..592efb714f3 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -10,6 +10,7 @@ class Note < ActiveRecord::Base
include Awardable
include Importable
include FasterCacheKeys
+ include Redactable
include CacheMarkdownField
include AfterCommitQueue
include ResolvableNote
@@ -33,6 +34,8 @@ class Note < ActiveRecord::Base
cache_markdown_field :note, pipeline: :note, issuable_state_filter_enabled: true
+ redact_field :note
+
# Aliases to make application_helper#edited_time_ago_with_tooltip helper work properly with notes.
# See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10392/diffs#note_28719102
alias_attribute :last_edited_at, :updated_at
@@ -114,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/personal_access_token.rb b/app/models/personal_access_token.rb
index 207146479c0..73a58f2420e 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -3,7 +3,7 @@
class PersonalAccessToken < ActiveRecord::Base
include Expirable
include TokenAuthenticatable
- add_authentication_token_field :token
+ add_authentication_token_field :token, digest: true, fallback: true
REDIS_EXPIRY_TIME = 3.minutes
@@ -33,16 +33,22 @@ class PersonalAccessToken < ActiveRecord::Base
def self.redis_getdel(user_id)
Gitlab::Redis::SharedState.with do |redis|
- token = redis.get(redis_shared_state_key(user_id))
+ encrypted_token = redis.get(redis_shared_state_key(user_id))
redis.del(redis_shared_state_key(user_id))
- token
+ begin
+ Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_token)
+ rescue => ex
+ logger.warn "Failed to decrypt PersonalAccessToken value stored in Redis for User ##{user_id}: #{ex.class}"
+ encrypted_token
+ end
end
end
def self.redis_store!(user_id, token)
+ encrypted_token = Gitlab::CryptoHelper.aes256_gcm_encrypt(token)
+
Gitlab::Redis::SharedState.with do |redis|
- redis.set(redis_shared_state_key(user_id), token, ex: REDIS_EXPIRY_TIME)
- token
+ redis.set(redis_shared_state_key(user_id), encrypted_token, ex: REDIS_EXPIRY_TIME)
end
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 382fb4f463a..4d1917b9ab2 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
@@ -1811,7 +1812,7 @@ class Project < ActiveRecord::Base
.first
end
- def secret_variables_for(ref:, environment: nil)
+ def ci_variables_for(ref:, environment: nil)
# EE would use the environment
if protected_for?(ref)
variables
@@ -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
@@ -1902,10 +1903,6 @@ class Project < ActiveRecord::Base
false
end
- def issue_board_milestone_available?(user = nil)
- feature_available?(:issue_board_milestone, user)
- end
-
def full_path_was
File.join(namespace.full_path, previous_changes['path'].first)
end
@@ -1967,7 +1964,7 @@ class Project < ActiveRecord::Base
end
def migrate_to_hashed_storage!
- return if hashed_storage?(:repository)
+ return unless storage_upgradable?
update!(repository_read_only: true)
@@ -2073,6 +2070,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 +2103,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/chat_message/push_message.rb b/app/models/project_services/chat_message/push_message.rb
index 82be33a12a1..5dd0414b7e6 100644
--- a/app/models/project_services/chat_message/push_message.rb
+++ b/app/models/project_services/chat_message/push_message.rb
@@ -26,16 +26,8 @@ module ChatMessage
end
def activity
- action = if new_branch?
- "created"
- elsif removed_branch?
- "removed"
- else
- "pushed to"
- end
-
{
- title: "#{user_combined_name} #{action} #{ref_type}",
+ title: humanized_action(short: true),
subtitle: "in #{project_link}",
text: compare_link,
image: user_avatar
@@ -44,32 +36,21 @@ module ChatMessage
private
+ def humanized_action(short: false)
+ action, ref_link, target_link = compose_action_details
+ text = [user_combined_name, action, ref_type, ref_link]
+ text << target_link unless short
+ text.join(' ')
+ end
+
def message
- if new_branch?
- new_branch_message
- elsif removed_branch?
- removed_branch_message
- else
- push_message
- end
+ humanized_action
end
def format(string)
Slack::Notifier::LinkFormatter.format(string)
end
- def new_branch_message
- "#{user_combined_name} pushed new #{ref_type} #{branch_link} to #{project_link}"
- end
-
- def removed_branch_message
- "#{user_combined_name} removed #{ref_type} #{ref} from #{project_link}"
- end
-
- def push_message
- "#{user_combined_name} pushed to #{ref_type} #{branch_link} of #{project_link} (#{compare_link})"
- end
-
def commit_messages
commits.map { |commit| compose_commit_message(commit) }.join("\n\n")
end
@@ -115,6 +96,16 @@ module ChatMessage
"[Compare changes](#{compare_url})"
end
+ def compose_action_details
+ if new_branch?
+ ['pushed new', branch_link, "to #{project_link}"]
+ elsif removed_branch?
+ ['removed', ref, "from #{project_link}"]
+ else
+ ['pushed to', branch_link, "of #{project_link} (#{compare_link})"]
+ end
+ end
+
def attachment_color
'#345'
end
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 f119555f16b..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|
@@ -144,7 +149,7 @@ class KubernetesService < DeploymentService
end
def kubeclient
- @kubeclient ||= build_kube_client!(api_groups: ['api', 'apis/rbac.authorization.k8s.io'])
+ @kubeclient ||= build_kube_client!
end
def deprecated?
@@ -182,13 +187,11 @@ class KubernetesService < DeploymentService
slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '')
end
- def build_kube_client!(api_groups: ['api'], api_version: 'v1')
+ def build_kube_client!
raise "Incomplete settings" unless api_url && actual_namespace && token
Gitlab::Kubernetes::KubeClient.new(
api_url,
- api_groups,
- api_version,
auth_options: kubeclient_auth_options,
ssl_options: kubeclient_ssl_options,
http_proxy_uri: ENV['http_proxy']
@@ -200,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..fff6d4be275 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
@@ -1041,11 +1049,19 @@ class Repository
end
def cache
- @cache ||= Gitlab::RepositoryCache.new(self)
+ @cache ||= if is_wiki
+ Gitlab::RepositoryCache.new(self, extra_namespace: 'wiki')
+ else
+ Gitlab::RepositoryCache.new(self)
+ end
end
def request_store_cache
- @request_store_cache ||= Gitlab::RepositoryCache.new(self, backend: Gitlab::SafeRequestStore)
+ @request_store_cache ||= if is_wiki
+ Gitlab::RepositoryCache.new(self, extra_namespace: 'wiki', backend: Gitlab::SafeRequestStore)
+ else
+ Gitlab::RepositoryCache.new(self, backend: Gitlab::SafeRequestStore)
+ end
end
def tags_sorted_by_committed_date
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 e9533ee7c77..11856b55902 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -2,6 +2,7 @@
class Snippet < ActiveRecord::Base
include Gitlab::VisibilityLevel
+ include Redactable
include CacheMarkdownField
include Noteable
include Participable
@@ -18,6 +19,8 @@ class Snippet < ActiveRecord::Base
cache_markdown_field :description
cache_markdown_field :content
+ redact_field :description
+
# Aliases to make application_helper#edited_time_ago_with_tooltip helper work properly with snippets.
# See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10392/diffs#note_28719102
alias_attribute :last_edited_at, :updated_at
@@ -60,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
@@ -78,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/ssh_host_key.rb b/app/models/ssh_host_key.rb
new file mode 100644
index 00000000000..b6844dbe870
--- /dev/null
+++ b/app/models/ssh_host_key.rb
@@ -0,0 +1,130 @@
+# frozen_string_literal: true
+
+# Detected SSH host keys are transiently stored in Redis
+class SshHostKey
+ class Fingerprint < Gitlab::SSHPublicKey
+ attr_reader :index
+
+ def initialize(key, index: nil)
+ super(key)
+
+ @index = index
+ end
+
+ def as_json(*)
+ { bits: bits, fingerprint: fingerprint, type: type, index: index }
+ end
+ end
+
+ include ReactiveCaching
+
+ self.reactive_cache_key = ->(key) { [key.class.to_s, key.id] }
+
+ # Do not refresh the data in the background - it is not expected to change.
+ # This is achieved by making the lifetime shorter than the refresh interval.
+ self.reactive_cache_refresh_interval = 15.minutes
+ self.reactive_cache_lifetime = 10.minutes
+
+ def self.find_by(opts = {})
+ return nil unless opts.key?(:id)
+
+ project_id, url = opts[:id].split(':', 2)
+ project = Project.find_by(id: project_id)
+
+ project.presence && new(project: project, url: url)
+ end
+
+ def self.fingerprint_host_keys(data)
+ return [] unless data.is_a?(String)
+
+ data
+ .each_line
+ .each_with_index
+ .map { |line, index| Fingerprint.new(line, index: index) }
+ .select(&:valid?)
+ end
+
+ attr_reader :project, :url, :compare_host_keys
+
+ def initialize(project:, url:, compare_host_keys: nil)
+ @project = project
+ @url = normalize_url(url)
+ @compare_host_keys = compare_host_keys
+ end
+
+ def id
+ [project.id, url].join(':')
+ end
+
+ def as_json(*)
+ {
+ host_keys_changed: host_keys_changed?,
+ fingerprints: fingerprints,
+ known_hosts: known_hosts
+ }
+ end
+
+ def known_hosts
+ with_reactive_cache { |data| data[:known_hosts] }
+ end
+
+ def fingerprints
+ @fingerprints ||= self.class.fingerprint_host_keys(known_hosts)
+ end
+
+ # Returns true if the known_hosts data differs from the version passed in at
+ # initialization as `compare_host_keys`. Comments, ordering, etc, is ignored
+ def host_keys_changed?
+ cleanup(known_hosts) != cleanup(compare_host_keys)
+ end
+
+ def error
+ with_reactive_cache { |data| data[:error] }
+ end
+
+ def calculate_reactive_cache
+ known_hosts, errors, status =
+ Open3.popen3({}, *%W[ssh-keyscan -T 5 -p #{url.port} -f-]) do |stdin, stdout, stderr, wait_thr|
+ stdin.puts(url.host)
+ stdin.close
+
+ [
+ cleanup(stdout.read),
+ cleanup(stderr.read),
+ wait_thr.value
+ ]
+ end
+
+ # ssh-keyscan returns an exit code 0 in several error conditions, such as an
+ # unknown hostname, so check both STDERR and the exit code
+ if status.success? && !errors.present?
+ { known_hosts: known_hosts }
+ else
+ Rails.logger.debug("Failed to detect SSH host keys for #{id}: #{errors}")
+
+ { error: 'Failed to detect SSH host keys' }
+ end
+ end
+
+ private
+
+ # Remove comments and duplicate entries
+ def cleanup(data)
+ data
+ .to_s
+ .each_line
+ .reject { |line| line.start_with?('#') || line.chomp.empty? }
+ .uniq
+ .sort
+ .join
+ end
+
+ def normalize_url(url)
+ full_url = ::Addressable::URI.parse(url)
+ raise ArgumentError.new("Invalid URL") unless full_url&.scheme == 'ssh'
+
+ Addressable::URI.parse("ssh://#{full_url.host}:#{full_url.inferred_port}")
+ rescue Addressable::URI::InvalidURIError
+ raise ArgumentError.new("Invalid URL")
+ end
+end
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 ca7fc3b058f..01eba7e0426 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -28,7 +28,7 @@ class User < ActiveRecord::Base
ignore_column :email_provider
ignore_column :authentication_token
- add_authentication_token_field :incoming_email_token
+ add_authentication_token_field :incoming_email_token, token_generator: -> { SecureRandom.hex.to_i(16).to_s(36) }
add_authentication_token_field :feed_token
default_value_for :admin, false
@@ -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,18 +347,36 @@ class User < ActiveRecord::Base
# Find a User by their primary email or any associated secondary email
def find_by_any_email(email, confirmed: false)
+ return unless email
+
by_any_email(email, confirmed: confirmed).take
end
- # Returns a relation containing all the users for the given Email address
- def by_any_email(email, confirmed: false)
- users = where(email: email)
- users = users.confirmed if confirmed
+ # Returns a relation containing all the users for the given email addresses
+ #
+ # @param emails [String, Array<String>] email addresses to check
+ # @param confirmed [Boolean] Only return users where the email is confirmed
+ def by_any_email(emails, confirmed: false)
+ emails = Array(emails).map(&:downcase)
+
+ from_users = where(email: emails)
+ from_users = from_users.confirmed if confirmed
+
+ from_emails = joins(:emails).where(emails: { email: emails })
+ from_emails = from_emails.confirmed if confirmed
+
+ items = [from_users, from_emails]
+
+ user_ids = Gitlab::PrivateCommitEmail.user_ids_for_emails(emails)
+ items << where(id: user_ids) if user_ids.present?
+
+ from_union(items)
+ end
- emails = joins(:emails).where(emails: { email: email })
- emails = emails.confirmed if confirmed
+ def find_by_private_commit_email(email)
+ user_id = Gitlab::PrivateCommitEmail.user_id_for_email(email)
- from_union([users, emails])
+ find_by(id: user_id)
end
def filter(filter_name)
@@ -460,12 +478,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 +651,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 +667,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 +961,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.
@@ -1014,6 +1039,7 @@ class User < ActiveRecord::Base
def all_emails
all_emails = []
all_emails << email unless temp_oauth_email?
+ all_emails << private_commit_email
all_emails.concat(emails.map(&:email))
all_emails
end
@@ -1021,13 +1047,29 @@ 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 any_email?(check_email)
+ downcased = check_email.downcase
+
+ # handle the outdated private commit email case
+ return true if persisted? &&
+ id == Gitlab::PrivateCommitEmail.user_id_for_email(downcased)
+
+ all_emails.include?(check_email.downcase)
+ end
+
def verified_email?(check_email)
downcased = check_email.downcase
- email == downcased ? primary_email_verified? : emails.confirmed.where(email: downcased).exists?
+
+ # handle the outdated private commit email case
+ return true if persisted? &&
+ id == Gitlab::PrivateCommitEmail.user_id_for_email(downcased)
+
+ verified_emails.include?(check_email.downcase)
end
def hook_attrs
@@ -1138,7 +1180,7 @@ class User < ActiveRecord::Base
events = Event.select(:project_id)
.contributions.where(author_id: self)
.where("created_at > ?", Time.now - 1.year)
- .uniq
+ .distinct
.reorder(nil)
Project.where(id: events)
@@ -1464,15 +1506,6 @@ class User < ActiveRecord::Base
end
end
- def generate_token(token_field)
- if token_field == :incoming_email_token
- # Needs to be all lowercase and alphanumeric because it's gonna be used in an email address.
- SecureRandom.hex.to_i(16).to_s(36)
- else
- super
- end
- end
-
def self.unique_internal(scope, username, email_pattern, &block)
scope.first || create_unique_internal(scope, username, email_pattern, &block)
end
diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb
index 1cd05cf3aac..76e7bc06b4e 100644
--- a/app/models/user_callout.rb
+++ b/app/models/user_callout.rb
@@ -3,11 +3,9 @@
class UserCallout < ActiveRecord::Base
belongs_to :user
- enum feature_name: {
- gke_cluster_integration: 1,
- gcp_signup_offer: 2,
- cluster_security_warning: 3
- }
+ # We use `UserCalloutEnums.feature_names` here so that EE can more easily
+ # extend this `Hash` with new values.
+ enum feature_name: ::UserCalloutEnums.feature_names
validates :user, presence: true
validates :feature_name,
diff --git a/app/models/user_callout_enums.rb b/app/models/user_callout_enums.rb
new file mode 100644
index 00000000000..b9373ae6166
--- /dev/null
+++ b/app/models/user_callout_enums.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module UserCalloutEnums
+ # Returns the `Hash` to use for the `feature_name` enum in the `UserCallout`
+ # model.
+ #
+ # This method is separate from the `UserCallout` model so that it can be
+ # extended by EE.
+ def self.feature_names
+ {
+ gke_cluster_integration: 1,
+ gcp_signup_offer: 2,
+ cluster_security_warning: 3
+ }
+ end
+end
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/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb
index f9623587957..e42d78f47c5 100644
--- a/app/policies/ci/pipeline_policy.rb
+++ b/app/policies/ci/pipeline_policy.rb
@@ -16,6 +16,10 @@ module Ci
enable :update_pipeline
end
+ rule { can?(:owner_access) }.policy do
+ enable :destroy_pipeline
+ end
+
def ref_protected?(user, project, tag, ref)
access = ::Gitlab::UserAccess.new(user, project: project)
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/ci/pipeline_presenter.rb b/app/presenters/ci/pipeline_presenter.rb
index 93a38f92073..57daf04efc6 100644
--- a/app/presenters/ci/pipeline_presenter.rb
+++ b/app/presenters/ci/pipeline_presenter.rb
@@ -4,9 +4,11 @@ module Ci
class PipelinePresenter < Gitlab::View::Presenter::Delegated
include Gitlab::Utils::StrongMemoize
- FAILURE_REASONS = {
- config_error: 'CI/CD YAML configuration error!'
- }.freeze
+ # We use a class method here instead of a constant, allowing EE to redefine
+ # the returned `Hash` more easily.
+ def self.failure_reasons
+ { config_error: 'CI/CD YAML configuration error!' }
+ end
presents :pipeline
@@ -21,7 +23,7 @@ module Ci
def failure_reason
return unless pipeline.failure_reason?
- FAILURE_REASONS[pipeline.failure_reason.to_sym] ||
+ self.class.failure_reasons[pipeline.failure_reason.to_sym] ||
pipeline.failure_reason
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 29eaad759bb..0cd77da6303 100644
--- a/app/presenters/commit_status_presenter.rb
+++ b/app/presenters/commit_status_presenter.rb
@@ -9,7 +9,9 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
runner_system_failure: 'There has been a runner system failure, please try again',
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'
+ stale_schedule: 'Delayed job could not be executed by some reason, please try again',
+ job_execution_timeout: 'The script exceeded the maximum execution time set for the job',
+ archived_failure: 'The job is archived and cannot be run'
}.freeze
private_constant :CALLOUT_FAILURE_MESSAGES
@@ -29,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/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index 3f565b826dd..1db6c9eff36 100644
--- a/app/presenters/merge_request_presenter.rb
+++ b/app/presenters/merge_request_presenter.rb
@@ -108,16 +108,10 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
namespace = source_project_namespace
branch = source_branch
- if source_branch_exists?
- namespace = link_to(namespace, project_path(source_project))
- branch = link_to(branch, project_tree_path(source_project, source_branch))
- end
+ namespace_link = source_branch_exists? ? link_to(namespace, project_path(source_project)) : ERB::Util.html_escape(namespace)
+ branch_link = source_branch_exists? ? link_to(branch, project_tree_path(source_project, source_branch)) : ERB::Util.html_escape(branch)
- if for_fork?
- namespace + ":" + branch
- else
- branch
- end
+ for_fork? ? "#{namespace_link}:#{branch_link}" : branch_link
end
def closing_issues_links
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/build_details_entity.rb b/app/serializers/build_details_entity.rb
index 066a5b1885c..9ddce0d2c80 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -5,6 +5,7 @@ class BuildDetailsEntity < JobEntity
expose :tag_list, as: :tags
expose :has_trace?, as: :has_trace
expose :stage
+ expose :stuck?, as: :stuck
expose :user, using: UserEntity
expose :runner, using: RunnerEntity
expose :pipeline, using: PipelineEntity
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 3dfa4f204c9..f6321b9e520 100644
--- a/app/serializers/environment_status_entity.rb
+++ b/app/serializers/environment_status_entity.rb
@@ -5,12 +5,13 @@ class EnvironmentStatusEntity < Grape::Entity
expose :id
expose :name
+ expose :status
expose :url do |es|
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
@@ -36,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
@@ -44,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/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index 9ec24f799ef..f33a1654d5e 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -55,6 +55,7 @@ class MergeRequestWidgetEntity < IssuableEntity
expose :merge_commit_message
expose :actual_head_pipeline, with: PipelineDetailsEntity, as: :pipeline
+ expose :merge_pipeline, with: PipelineDetailsEntity, if: ->(mr, _) { mr.merged? && can?(request.current_user, :read_pipeline, mr.target_project)}
# Booleans
expose :merge_ongoing?, as: :merge_ongoing
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/boards/visits/create_service.rb b/app/services/boards/visits/create_service.rb
new file mode 100644
index 00000000000..e2adf755511
--- /dev/null
+++ b/app/services/boards/visits/create_service.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Boards
+ module Visits
+ class CreateService < Boards::BaseService
+ def execute(board)
+ return unless current_user && Gitlab::Database.read_write?
+
+ if parent.is_a?(Group)
+ BoardGroupRecentVisit.visited!(current_user, board)
+ else
+ BoardProjectRecentVisit.visited!(current_user, board)
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/boards/visits/latest_service.rb b/app/services/boards/visits/latest_service.rb
new file mode 100644
index 00000000000..9e4c77a6317
--- /dev/null
+++ b/app/services/boards/visits/latest_service.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Boards
+ module Visits
+ class LatestService < Boards::BaseService
+ def execute
+ return nil unless current_user
+
+ if parent.is_a?(Group)
+ BoardGroupRecentVisit.latest(current_user, parent)
+ else
+ BoardProjectRecentVisit.latest(current_user, parent)
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/ci/destroy_pipeline_service.rb b/app/services/ci/destroy_pipeline_service.rb
new file mode 100644
index 00000000000..13f892aabb8
--- /dev/null
+++ b/app/services/ci/destroy_pipeline_service.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Ci
+ class DestroyPipelineService < BaseService
+ def execute(pipeline)
+ raise Gitlab::Access::AccessDeniedError unless can?(current_user, :destroy_pipeline, pipeline)
+
+ AuditEventService.new(current_user, pipeline).security_event
+
+ pipeline.destroy!
+ end
+ end
+end
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 c6e955800af..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.new(_('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, 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 3ae0a4a19d0..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
@@ -60,18 +77,15 @@ module Clusters
'https://' + gke_cluster.endpoint,
Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate),
gke_cluster.master_auth.username,
- gke_cluster.master_auth.password,
- api_groups: ['api', 'apis/rbac.authorization.k8s.io']
+ gke_cluster.master_auth.password
)
end
- def build_kube_client!(api_url, ca_pem, username, password, api_groups: ['api'], api_version: 'v1')
+ def build_kube_client!(api_url, ca_pem, username, password)
raise "Incomplete settings" unless api_url && username && password
Gitlab::Kubernetes::KubeClient.new(
api_url,
- api_groups,
- api_version,
auth_options: { username: username, password: password },
ssl_options: kubeclient_ssl_options(ca_pem),
http_proxy_uri: ENV['http_proxy']
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..b31426556f6
--- /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.set_defaults
+ 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/delete_merged_branches_service.rb b/app/services/delete_merged_branches_service.rb
index ced87a1c37a..80de897e94b 100644
--- a/app/services/delete_merged_branches_service.rb
+++ b/app/services/delete_merged_branches_service.rb
@@ -24,8 +24,8 @@ class DeleteMergedBranchesService < BaseService
# rubocop: disable CodeReuse/ActiveRecord
def merge_request_branch_names
# reorder(nil) is necessary for SELECT DISTINCT because default scope adds an ORDER BY
- source_names = project.origin_merge_requests.opened.reorder(nil).uniq.pluck(:source_branch)
- target_names = project.merge_requests.opened.reorder(nil).uniq.pluck(:target_branch)
+ source_names = project.origin_merge_requests.opened.reorder(nil).distinct.pluck(:source_branch)
+ target_names = project.merge_requests.opened.reorder(nil).distinct.pluck(:target_branch)
(source_names + target_names).uniq
end
# rubocop: enable CodeReuse/ActiveRecord
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..a1d0cc0e568 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -11,6 +11,12 @@ module Issues
move_issue_to_new_project(issue) || update(issue)
end
+ def update(issue)
+ create_merge_request_from_quick_action
+
+ super
+ end
+
def before_update(issue)
spam_check(issue, current_user)
end
@@ -48,6 +54,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 +99,25 @@ module Issues
private
+ def create_merge_request_from_quick_action
+ create_merge_request_params = params.delete(:create_merge_request)
+ return unless create_merge_request_params
+
+ MergeRequests::CreateFromIssueService.new(project, current_user, create_merge_request_params).execute
+ end
+
+ 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/labels/transfer_service.rb b/app/services/labels/transfer_service.rb
index 52360f775dc..9cbc9fef529 100644
--- a/app/services/labels/transfer_service.rb
+++ b/app/services/labels/transfer_service.rb
@@ -40,7 +40,7 @@ module Labels
group_labels_applied_to_merge_requests
])
.reorder(nil)
- .uniq
+ .distinct
end
# rubocop: enable CodeReuse/ActiveRecord
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/create_from_issue_service.rb b/app/services/merge_requests/create_from_issue_service.rb
index 020af0bb950..e69791872cc 100644
--- a/app/services/merge_requests/create_from_issue_service.rb
+++ b/app/services/merge_requests/create_from_issue_service.rb
@@ -19,13 +19,15 @@ module MergeRequests
result = CreateBranchService.new(project, current_user).execute(branch_name, ref)
return result if result[:status] == :error
- SystemNoteService.new_issue_branch(issue, project, current_user, branch_name)
-
new_merge_request = create(merge_request)
if new_merge_request.valid?
+ SystemNoteService.new_merge_request(issue, project, current_user, new_merge_request)
+
success(new_merge_request)
else
+ SystemNoteService.new_issue_branch(issue, project, current_user, branch_name)
+
error(new_merge_request.errors)
end
end
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index fb44f809c41..70a67baa01c 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -49,6 +49,11 @@ module MergeRequests
end
end
+ # Overridden in EE.
+ def hooks_validation_pass?(_merge_request)
+ true
+ end
+
private
def error_check!
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index b03d14fa3cc..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
@@ -85,13 +85,10 @@ module MergeRequests
.where.not(target_project: @project).to_a
filter_merge_requests(merge_requests).each do |merge_request|
- if merge_request.source_branch == @push.branch_name || @push.force_push?
+ if branch_and_project_match?(merge_request) || @push.force_push?
+ merge_request.reload_diff(current_user)
+ elsif merge_request.includes_any_commits?(push_commit_ids)
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?
end
merge_request.mark_as_unchecked
@@ -104,6 +101,15 @@ 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
+ end
+
def reset_merge_when_pipeline_succeeds
merge_requests_for_source_branch.each(&:reset_merge_when_pipeline_succeeds)
end
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/disable_deploy_key_service.rb b/app/services/projects/disable_deploy_key_service.rb
new file mode 100644
index 00000000000..e483c0708c4
--- /dev/null
+++ b/app/services/projects/disable_deploy_key_service.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Projects
+ class DisableDeployKeyService < BaseService
+ def execute
+ # rubocop: disable CodeReuse/ActiveRecord
+ deploy_key_project = project.deploy_keys_projects.find_by(deploy_key_id: params[:id])
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ deploy_key_project&.destroy!
+ end
+ end
+end
diff --git a/app/services/projects/enable_deploy_key_service.rb b/app/services/projects/enable_deploy_key_service.rb
index 102088e9557..38219cacee9 100644
--- a/app/services/projects/enable_deploy_key_service.rb
+++ b/app/services/projects/enable_deploy_key_service.rb
@@ -2,9 +2,10 @@
module Projects
class EnableDeployKeyService < BaseService
- # rubocop: disable CodeReuse/ActiveRecord
def execute
- key = accessible_keys.find_by(id: params[:key_id] || params[:id])
+ key_id = params[:key_id] || params[:id]
+ key = find_accessible_key(key_id)
+
return unless key
unless project.deploy_keys.include?(key)
@@ -13,12 +14,15 @@ module Projects
key
end
- # rubocop: enable CodeReuse/ActiveRecord
private
- def accessible_keys
- current_user.accessible_deploy_keys
+ def find_accessible_key(key_id)
+ if current_user.admin?
+ DeployKey.find_by_id(key_id)
+ else
+ current_user.accessible_deploy_keys.find_by_id(key_id)
+ end
end
end
end
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..d248b10f41e 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
@@ -635,6 +635,22 @@ module QuickActions
@updates[:tag_message] = message
end
+ desc 'Create a merge request.'
+ explanation do |branch_name = nil|
+ branch_text = branch_name ? "branch '#{branch_name}'" : 'a branch'
+ "Creates #{branch_text} and a merge request to resolve this issue"
+ end
+ params "<branch name>"
+ condition do
+ issuable.is_a?(Issue) && current_user.can?(:create_merge_request_in, project) && current_user.can?(:push_code, project)
+ end
+ command :create_merge_request do |branch_name = nil|
+ @updates[:create_merge_request] = {
+ branch_name: branch_name,
+ issue_iid: issuable.iid
+ }
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def extract_users(params)
return [] if params.nil?
diff --git a/app/services/resource_events/merge_into_notes_service.rb b/app/services/resource_events/merge_into_notes_service.rb
index 596c0105ea0..7504773a002 100644
--- a/app/services/resource_events/merge_into_notes_service.rb
+++ b/app/services/resource_events/merge_into_notes_service.rb
@@ -34,7 +34,7 @@ module ResourceEvents
def label_events_by_discussion_id
return [] unless resource.respond_to?(:resource_label_events)
- events = resource.resource_label_events.includes(:label, :user)
+ events = resource.resource_label_events.includes(:label, user: :status)
events = since_fetch_at(events)
events.group_by { |event| event.discussion_id }
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/system_note_service.rb b/app/services/system_note_service.rb
index 729bc991294..ec6c306227b 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -407,11 +407,17 @@ module SystemNoteService
def new_issue_branch(issue, project, author, branch)
link = url_helpers.project_compare_url(project, from: project.default_branch, to: branch)
- body = "created branch [`#{branch}`](#{link})"
+ body = "created branch [`#{branch}`](#{link}) to address this issue"
create_note(NoteSummary.new(issue, project, author, body, action: 'branch'))
end
+ def new_merge_request(issue, project, author, merge_request)
+ body = "created merge request #{merge_request.to_reference} to address this issue"
+
+ create_note(NoteSummary.new(issue, project, author, body, action: 'merge'))
+ end
+
# Called when a Mentionable references a Noteable
#
# noteable - Noteable object being referenced
diff --git a/app/services/todos/destroy/entity_leave_service.rb b/app/services/todos/destroy/entity_leave_service.rb
index e8d1bcdd142..ebfb20132d0 100644
--- a/app/services/todos/destroy/entity_leave_service.rb
+++ b/app/services/todos/destroy/entity_leave_service.rb
@@ -45,7 +45,7 @@ module Todos
# rubocop: disable CodeReuse/ActiveRecord
def remove_confidential_issue_todos
Todo.where(
- target_id: confidential_issues.select(:id), target_type: Issue, user_id: user.id
+ target_id: confidential_issues.select(:id), target_type: Issue.name, user_id: user.id
).delete_all
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/services/todos/destroy/private_features_service.rb b/app/services/todos/destroy/private_features_service.rb
index a8c3fe0ef5a..bd49519d694 100644
--- a/app/services/todos/destroy/private_features_service.rb
+++ b/app/services/todos/destroy/private_features_service.rb
@@ -14,9 +14,9 @@ module Todos
def execute
ProjectFeature.where(project_id: project_ids).each do |project_features|
target_types = []
- target_types << Issue if private?(project_features.issues_access_level)
- target_types << MergeRequest if private?(project_features.merge_requests_access_level)
- target_types << Commit if private?(project_features.repository_access_level)
+ target_types << Issue.name if private?(project_features.issues_access_level)
+ target_types << MergeRequest.name if private?(project_features.merge_requests_access_level)
+ target_types << Commit.name if private?(project_features.repository_access_level)
next if target_types.empty?
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..3f503f3da28 100644
--- a/app/services/users/build_service.rb
+++ b/app/services/users/build_service.rb
@@ -28,7 +28,7 @@ module Users
identity_attrs = params.slice(:extern_uid, :provider)
- if identity_attrs.any?
+ unless identity_attrs.empty?
user.identities.build(identity_attrs)
end
@@ -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..a0a00ac5d96 100644
--- a/app/views/admin/background_jobs/show.html.haml
+++ b/app/views/admin/background_jobs/show.html.haml
@@ -6,40 +6,5 @@
%p.light GitLab uses #{link_to "sidekiq", "http://sidekiq.org/"} library for async job processing
%hr
-
- .card
- .card-header Sidekiq running processes
- .card-body
- - if @sidekiq_processes.empty?
- %h4.cred
- %i.fa.fa-exclamation-triangle
- There are no running sidekiq processes. Please restart GitLab
- - else
- .table-holder
- %table.table
- %thead
- %th USER
- %th PID
- %th CPU
- %th MEM
- %th STATE
- %th START
- %th COMMAND
- %tbody
- - @sidekiq_processes.each do |process|
- %tr
- %td= gitlab_config.user
- - parse_sidekiq_ps(process).each do |value|
- %td= value
- .clearfix
- %p
- %i.fa.fa-exclamation-circle
- If '[#{@concurrency} of #{@concurrency} busy]' is shown, restart GitLab with 'sudo service gitlab reload'.
- %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.
-
-
-
.card
%iframe{ src: sidekiq_path, width: '100%', height: 970, style: "border: 0" }
diff --git a/app/views/admin/impersonation_tokens/index.html.haml b/app/views/admin/impersonation_tokens/index.html.haml
index 9e490713ef3..8e869fb4b71 100644
--- a/app/views/admin/impersonation_tokens/index.html.haml
+++ b/app/views/admin/impersonation_tokens/index.html.haml
@@ -5,6 +5,11 @@
.row.prepend-top-default
.col-lg-12
+ - if @new_impersonation_token
+ = render "shared/personal_access_tokens_created_container", new_token_value: @new_impersonation_token,
+ container_title: 'Your New Impersonation Token',
+ clipboard_button_title: 'Copy impersonation token to clipboard'
+
= render "shared/personal_access_tokens_form", path: admin_user_impersonation_tokens_path, impersonation: true, token: @impersonation_token, scopes: @scopes
= render "shared/personal_access_tokens_table", impersonation: true, active_tokens: @active_impersonation_tokens, inactive_tokens: @inactive_impersonation_tokens
diff --git a/app/views/ci/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml
index e402801a776..f34305e94fa 100644
--- a/app/views/ci/variables/_index.html.haml
+++ b/app/views/ci/variables/_index.html.haml
@@ -9,8 +9,8 @@
= render 'ci/variables/variable_row', form_field: 'variables', variable: variable
= render 'ci/variables/variable_row', form_field: 'variables'
.prepend-top-20
- %button.btn.btn-success.js-secret-variables-save-button{ type: 'button' }
- %span.hide.js-secret-variables-save-loading-icon
+ %button.btn.btn-success.js-ci-variables-save-button{ type: 'button' }
+ %span.hide.js-ci-variables-save-loading-icon
= icon('spinner spin')
= _('Save variables')
%button.btn.btn-info.btn-inverted.prepend-left-10.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: "#{@variables.size == 0}" } }
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/issues.html.haml b/app/views/dashboard/issues.html.haml
index 832ba877558..fdd5c19d562 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -1,6 +1,6 @@
- @hide_top_links = true
- page_title _("Issues")
-- @breadcrumb_link = issues_dashboard_path(assignee_id: current_user.id)
+- @breadcrumb_link = issues_dashboard_path(assignee_username: current_user.username)
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{current_user.name} issues")
@@ -16,7 +16,7 @@
.nav-controls
= render 'shared/issuable/feed_buttons'
-= render 'shared/issuable/filter', type: :issues
+= render 'shared/issuable/search_bar', type: :issues
- if current_user && @no_filters_set
= render 'shared/dashboard/no_filter_selected'
diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml
index fba8d1cf667..77cfa1271df 100644
--- a/app/views/dashboard/merge_requests.html.haml
+++ b/app/views/dashboard/merge_requests.html.haml
@@ -1,6 +1,6 @@
- @hide_top_links = true
- page_title _("Merge Requests")
-- @breadcrumb_link = merge_requests_dashboard_path(assignee_id: current_user.id)
+- @breadcrumb_link = merge_requests_dashboard_path(assignee_username: current_user.username)
.page-title-holder
%h1.page-title= _('Merge Requests')
@@ -12,7 +12,7 @@
.top-area
= render 'shared/issuable/nav', type: :merge_requests, display_count: !@no_filters_set
-= render 'shared/issuable/filter', type: :merge_requests
+= render 'shared/issuable/search_bar', type: :merge_requests
- if current_user && @no_filters_set
= render 'shared/dashboard/no_filter_selected'
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/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml
index 5ddb3ece1cb..ec968e435cd 100644
--- a/app/views/devise/shared/_signin_box.html.haml
+++ b/app/views/devise/shared/_signin_box.html.haml
@@ -1,10 +1,10 @@
- if form_based_providers.any?
- if crowd_enabled?
- .login-box.tab-pane.active{ id: "crowd", role: 'tabpanel' }
+ .login-box.tab-pane{ id: "crowd", role: 'tabpanel', class: active_when(form_based_auth_provider_has_active_class?(:crowd)) }
.login-body
= render 'devise/sessions/new_crowd'
- @ldap_servers.each_with_index do |server, i|
- .login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: active_when(i.zero? && !crowd_enabled?) }
+ .login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: active_when(i.zero? && form_based_auth_provider_has_active_class?(:ldapmain)) }
.login-body
= render 'devise/sessions/new_ldap', server: server
- if password_authentication_enabled_for_web?
@@ -12,6 +12,8 @@
.login-body
= render 'devise/sessions/new_base'
+ = render_if_exists 'devise/sessions/new_smartcard'
+
- elsif password_authentication_enabled_for_web?
.login-box.tab-pane.active{ id: 'login-pane', role: 'tabpanel' }
.login-body
diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml
index 7dced0942f5..aee05b6c81c 100644
--- a/app/views/devise/shared/_tabs_ldap.html.haml
+++ b/app/views/devise/shared/_tabs_ldap.html.haml
@@ -1,10 +1,13 @@
%ul.nav-links.new-session-tabs.nav-tabs.nav{ class: ('custom-provider-tabs' if form_based_providers.any?) }
- if crowd_enabled?
%li.nav-item
- = link_to "Crowd", "#crowd", class: 'nav-link active', 'data-toggle' => 'tab'
+ = link_to "Crowd", "#crowd", class: "nav-link #{active_when(form_based_auth_provider_has_active_class?(:crowd))}", 'data-toggle' => 'tab'
- @ldap_servers.each_with_index do |server, i|
%li.nav-item
- = link_to server['label'], "##{server['provider_name']}", class: "nav-link #{active_when(i.zero? && !crowd_enabled?)} qa-ldap-tab", 'data-toggle' => 'tab'
+ = link_to server['label'], "##{server['provider_name']}", class: "nav-link #{active_when(i.zero? && form_based_auth_provider_has_active_class?(:ldapmain))} qa-ldap-tab", 'data-toggle' => 'tab'
+
+ = render_if_exists 'devise/shared/tab_smartcard'
+
- if password_authentication_enabled_for_web?
%li.nav-item
= link_to 'Standard', '#login-pane', class: 'nav-link qa-standard-tab', 'data-toggle' => 'tab'
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/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml
index 647948c7dff..a5e6abdba52 100644
--- a/app/views/groups/settings/ci_cd/show.html.haml
+++ b/app/views/groups/settings/ci_cd/show.html.haml
@@ -3,7 +3,7 @@
- expanded = Rails.env.test?
-%section.settings#secret-variables.no-animate{ class: ('expanded' if expanded) }
+%section.settings#ci-variables.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Variables')
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 596fc3985b3..474ef25cef7 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -5,7 +5,7 @@
- else
- search_path_url = search_path
-%header.navbar.navbar-gitlab.qa-navbar.navbar-expand-sm
+%header.navbar.navbar-gitlab.qa-navbar.navbar-expand-sm.js-navbar
%a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content
.container-fluid
.header-content
@@ -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/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index c10d4ea1a4d..c1e1eaff942 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -14,17 +14,7 @@
.col-lg-8
- if @new_personal_access_token
- .created-personal-access-token-container
- %h5.prepend-top-0
- Your New Personal Access Token
- .form-group
- .input-group
- = text_field_tag 'created-personal-access-token', @new_personal_access_token, readonly: true, class: "form-control js-select-on-focus", 'aria-describedby' => "created-personal-access-token-help-block"
- %span.input-group-append
- = clipboard_button(text: @new_personal_access_token, title: "Copy personal access token to clipboard", placement: "left", class: "input-group-text btn-default btn-clipboard")
- %span#created-personal-access-token-help-block.form-text.text-muted.text-danger Make sure you save it - you won't be able to access it again.
-
- %hr
+ = render "shared/personal_access_tokens_created_container", new_token_value: @new_personal_access_token
= render "shared/personal_access_tokens_form", path: profile_personal_access_tokens_path, impersonation: false, token: @personal_access_token, scopes: @scopes
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/environments/_external_url.html.haml b/app/views/projects/environments/_external_url.html.haml
index b3a82d1ef41..82567f88ccc 100644
--- a/app/views/projects/environments/_external_url.html.haml
+++ b/app/views/projects/environments/_external_url.html.haml
@@ -1,4 +1,4 @@
- if environment.external_url && can?(current_user, :read_environment, environment)
= link_to environment.external_url, target: '_blank', rel: 'noopener noreferrer', class: 'btn external-url has-tooltip qa-view-deployment', title: s_('Environments|Open live environment') do
= sprite_icon('external-link')
- View deployment
+ = _("View deployment")
diff --git a/app/views/projects/environments/_form.html.haml b/app/views/projects/environments/_form.html.haml
index f942b936037..cbd5c54cecc 100644
--- a/app/views/projects/environments/_form.html.haml
+++ b/app/views/projects/environments/_form.html.haml
@@ -1,22 +1,21 @@
.row.prepend-top-default.append-bottom-default
.col-lg-3
%h4.prepend-top-0
- Environments
+ = _("Environments")
%p
- Environments allow you to track deployments of your application
- = succeed "." do
- = link_to "Read more about environments", help_page_path("ci/environments")
+ - link_to_read_more = link_to(_("Read more about environments"), help_page_path("ci/environments"))
+ = _("Environments allow you to track deployments of your application %{link_to_read_more}.").html_safe % { link_to_read_more: link_to_read_more }
= form_for [@project.namespace.becomes(Namespace), @project, @environment], html: { class: 'col-lg-9' } do |f|
= form_errors(@environment)
.form-group
- = f.label :name, 'Name', class: 'label-bold'
+ = f.label :name, _('Name'), class: 'label-bold'
= f.text_field :name, required: true, class: 'form-control'
.form-group
- = f.label :external_url, 'External URL', class: 'label-bold'
+ = f.label :external_url, _('External URL'), class: 'label-bold'
= f.url_field :external_url, class: 'form-control'
.form-actions
- = f.submit 'Save', class: 'btn btn-success'
- = link_to 'Cancel', project_environments_path(@project), class: 'btn btn-cancel'
+ = f.submit _('Save'), class: 'btn btn-save'
+ = link_to _('Cancel'), project_environments_path(@project), class: 'btn btn-cancel'
diff --git a/app/views/projects/environments/_metrics_button.html.haml b/app/views/projects/environments/_metrics_button.html.haml
index a4b27575095..c4f19ea79e7 100644
--- a/app/views/projects/environments/_metrics_button.html.haml
+++ b/app/views/projects/environments/_metrics_button.html.haml
@@ -2,6 +2,6 @@
- return unless can?(current_user, :read_environment, environment)
-= link_to environment_metrics_path(environment), title: 'See metrics', class: 'btn metrics-button' do
+= link_to environment_metrics_path(environment), title: _('See metrics'), class: 'btn metrics-button' do
= sprite_icon('chart')
- Monitoring
+ = _("Monitoring")
diff --git a/app/views/projects/environments/edit.html.haml b/app/views/projects/environments/edit.html.haml
index d6ff3f729b4..d581bd3aeab 100644
--- a/app/views/projects/environments/edit.html.haml
+++ b/app/views/projects/environments/edit.html.haml
@@ -1,8 +1,8 @@
- @no_container = true
-- page_title "Edit", @environment.name, "Environments"
+- page_title _("Edit"), @environment.name, _("Environments")
%div{ class: container_class }
%h3.page-title
- Edit environment
+ = _('Edit environment')
%hr
= render 'form'
diff --git a/app/views/projects/environments/folder.html.haml b/app/views/projects/environments/folder.html.haml
index 1ac7dab6775..b7e1cf85cb7 100644
--- a/app/views/projects/environments/folder.html.haml
+++ b/app/views/projects/environments/folder.html.haml
@@ -1,5 +1,5 @@
- @no_container = true
-- page_title "Environments"
+- page_title _("Environments")
#environments-folder-list-view{ data: { endpoint: folder_project_environments_path(@project, @folder, format: :json),
"folder-name" => @folder,
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index 7ebe617766f..6c0ad34c486 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -1,6 +1,6 @@
- @no_container = true
-- page_title "Environments"
-- add_to_breadcrumbs("Pipelines", project_pipelines_path(@project))
+- page_title _("Environments")
+- add_to_breadcrumbs(_("Pipelines"), project_pipelines_path(@project))
#environments-list-view{ data: { environments_data: environments_list_data,
"can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml
index 4222963a754..7b847a85686 100644
--- a/app/views/projects/environments/metrics.html.haml
+++ b/app/views/projects/environments/metrics.html.haml
@@ -1,5 +1,5 @@
- @no_container = true
-- page_title "Metrics for environment", @environment.name
+- page_title _("Metrics for environment"), @environment.name
.prometheus-container{ class: container_class }
#prometheus-graphs{ data: metrics_data(@project, @environment) }
diff --git a/app/views/projects/environments/new.html.haml b/app/views/projects/environments/new.html.haml
index 62b08e85e22..c1067fdff78 100644
--- a/app/views/projects/environments/new.html.haml
+++ b/app/views/projects/environments/new.html.haml
@@ -1,9 +1,9 @@
- @no_container = true
-- breadcrumb_title "Environments"
-- page_title 'New Environment'
+- breadcrumb_title _("Environments")
+- page_title _("New Environment")
%div{ class: container_class }
%h3.page-title
- New environment
+ = _("New environment")
%hr
= render 'form'
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index 8c5b6e089ea..d59b2d4fb01 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -1,7 +1,7 @@
- @no_container = true
-- add_to_breadcrumbs "Environments", project_environments_path(@project)
+- add_to_breadcrumbs _("Environments"), project_environments_path(@project)
- breadcrumb_title @environment.name
-- page_title "Environments"
+- page_title _("Environments")
%div{ class: container_class }
- if can?(current_user, :stop_environment, @environment)
@@ -10,7 +10,7 @@
.modal-content
.modal-header
%h4.modal-title.d-flex.mw-100
- Stopping
+ = s_("Environments|Stopping")
%span.has-tooltip.text-truncate.ml-1.mr-1.flex-fill{ title: @environment.name, data: { container: '#stop-environment-modal' } }
= @environment.name
?
@@ -40,7 +40,7 @@
= render 'projects/environments/external_url', environment: @environment
= render 'projects/environments/metrics_button', environment: @environment
- if can?(current_user, :update_environment, @environment)
- = link_to 'Edit', edit_project_environment_path(@project, @environment), class: 'btn'
+ = link_to _('Edit'), edit_project_environment_path(@project, @environment), class: 'btn'
- if can?(current_user, :stop_environment, @environment)
= button_tag class: 'btn btn-danger', type: 'button', data: { toggle: 'modal',
target: '#stop-environment-modal' } do
@@ -52,21 +52,19 @@
.empty-state
.text-content
%h4.state-title
- You don't have any deployments right now.
+ = _("You don't have any deployments right now.")
%p.blank-state-text
- Define environments in the deploy stage(s) in
- %code .gitlab-ci.yml
- to track deployments here.
+ = _("Define environments in the deploy stage(s) in <code>.gitlab-ci.yml</code> to track deployments here.").html_safe
.text-center
= link_to _("Read more"), help_page_path("ci/environments"), class: "btn btn-success"
- else
.table-holder
.ci-table.environments{ role: 'grid' }
.gl-responsive-table-row.table-row-header{ role: 'row' }
- .table-section.section-10{ role: 'columnheader' } ID
- .table-section.section-30{ role: 'columnheader' } Commit
- .table-section.section-25{ role: 'columnheader' } Job
- .table-section.section-15{ role: 'columnheader' } Created
+ .table-section.section-10{ role: 'columnheader' }= _('ID')
+ .table-section.section-30{ role: 'columnheader' }= _('Commit')
+ .table-section.section-25{ role: 'columnheader' }= _('Job')
+ .table-section.section-15{ role: 'columnheader' }= _('Created')
= render @deployments
diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml
index e40d631a1a1..e837d3d56ac 100644
--- a/app/views/projects/environments/terminal.html.haml
+++ b/app/views/projects/environments/terminal.html.haml
@@ -1,5 +1,5 @@
- @no_container = true
-- page_title "Terminal for environment", @environment.name
+- page_title _("Terminal for environment"), @environment.name
- content_for :page_specific_javascripts do
= stylesheet_link_tag "xterm.css"
@@ -9,7 +9,7 @@
.row
.col-sm-6
%h3.page-title
- Terminal for environment
+ = _("Terminal for environment")
= @environment.name
.col-sm-6
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 c63ff070f70..66e202103a9 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -19,30 +19,23 @@
#js-pipeline-graph-vue
#js-tab-builds.tab-pane
- - if pipeline.yaml_errors.present?
- .bs-callout.bs-callout-danger
- %h4 Found errors in your .gitlab-ci.yml:
- %ul
- - pipeline.yaml_errors.split(",").each do |error|
- %li= error
- You can also test your .gitlab-ci.yml in the #{link_to "Lint", project_ci_lint_path(@project)}
+ - if pipeline.legacy_stages.present?
+ .table-holder.pipeline-holder
+ %table.table.ci-table.pipeline
+ %thead
+ %tr
+ %th Status
+ %th Job ID
+ %th Name
+ %th
+ %th Coverage
+ %th
+ = render partial: "projects/stage/stage", collection: pipeline.legacy_stages, as: :stage
- - if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file
+ - elsif pipeline.project.builds_enabled? && !pipeline.ci_yaml_file
.bs-callout.bs-callout-warning
\.gitlab-ci.yml not found in this commit
- .table-holder.pipeline-holder
- %table.table.ci-table.pipeline
- %thead
- %tr
- %th Status
- %th Job ID
- %th Name
- %th
- %th Coverage
- %th
- = render partial: "projects/stage/stage", collection: pipeline.legacy_stages, as: :stage
-
- if @pipeline.failed_builds.present?
#js-tab-failures.build-failures.tab-pane.build-page
%table.table.responsive-table.ci-table.responsive-table-sm-rounded
@@ -68,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/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml
index ff0ed3ed30d..193d437dad1 100644
--- a/app/views/projects/pipelines/show.html.haml
+++ b/app/views/projects/pipelines/show.html.haml
@@ -9,6 +9,14 @@
- if @pipeline.commit.present?
= render "projects/pipelines/info", commit: @pipeline.commit
- = render "projects/pipelines/with_tabs", pipeline: @pipeline
+ - if @pipeline.builds.empty? && @pipeline.yaml_errors.present?
+ .bs-callout.bs-callout-danger
+ %h4 Found errors in your .gitlab-ci.yml:
+ %ul
+ - @pipeline.yaml_errors.split(",").each do |error|
+ %li= error
+ You can test your .gitlab-ci.yml in #{link_to "CI Lint", project_ci_lint_path(@project)}.
+ - else
+ = render "projects/pipelines/with_tabs", pipeline: @pipeline
.js-pipeline-details-vue{ data: { endpoint: project_pipeline_path(@project, @pipeline, format: :json) } }
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_commit_column.html.haml b/app/views/projects/tree/_tree_commit_column.html.haml
index 406dccb74fb..e37fd7624be 100644
--- a/app/views/projects/tree/_tree_commit_column.html.haml
+++ b/app/views/projects/tree/_tree_commit_column.html.haml
@@ -1,2 +1,2 @@
%span.str-truncated
- = link_to_html commit.redacted_full_title_html, project_commit_path(@project, commit.id), class: 'tree-commit-link'
+ = link_to_html commit.redacted_full_title_html, project_commit_path(@project, commit.id), title: commit.redacted_full_title_html, class: 'tree-commit-link'
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/_labels_row.html.haml b/app/views/shared/_labels_row.html.haml
deleted file mode 100644
index 21b37a7c9ae..00000000000
--- a/app/views/shared/_labels_row.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-- labels.each do |label|
- %span.label-row.btn-group{ role: "group", aria: { label: label.name }, style: "color: #{text_color_for_bg(label.color)}" }
- = link_to_label(label, subject: @project, css_class: 'btn btn-transparent')
- %button.btn.btn-transparent.label-remove.js-label-filter-remove{ type: "button", style: "background-color: #{label.color};", data: { label: label.title } }
- = icon("times")
diff --git a/app/views/shared/_personal_access_tokens_created_container.html.haml b/app/views/shared/_personal_access_tokens_created_container.html.haml
new file mode 100644
index 00000000000..3150d39b84a
--- /dev/null
+++ b/app/views/shared/_personal_access_tokens_created_container.html.haml
@@ -0,0 +1,14 @@
+- container_title = local_assigns.fetch(:container_title, 'Your New Personal Access Token')
+- clipboard_button_title = local_assigns.fetch(:clipboard_button_title, 'Copy personal access token to clipboard')
+
+.created-personal-access-token-container
+ %h5.prepend-top-0
+ = container_title
+ .form-group
+ .input-group
+ = text_field_tag 'created-personal-access-token', new_token_value, readonly: true, class: "form-control js-select-on-focus", 'aria-describedby' => "created-token-help-block"
+ %span.input-group-append
+ = clipboard_button(text: new_token_value, title: clipboard_button_title, placement: "left", class: "input-group-text btn-default btn-clipboard")
+ %span#created-token-help-block.form-text.text-muted.text-danger Make sure you save it - you won't be able to access it again.
+
+%hr
diff --git a/app/views/shared/_personal_access_tokens_table.html.haml b/app/views/shared/_personal_access_tokens_table.html.haml
index cadac1cc99d..2efd03d4867 100644
--- a/app/views/shared/_personal_access_tokens_table.html.haml
+++ b/app/views/shared/_personal_access_tokens_table.html.haml
@@ -15,8 +15,6 @@
%th Created
%th Expires
%th Scopes
- - if impersonation
- %th Token
%th
%tbody
- active_tokens.each do |token|
@@ -30,10 +28,6 @@
- else
%span.token-never-expires-label Never
%td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>"
- - if impersonation
- %td.token-token-container
- = text_field_tag 'impersonation-token-token', token.token, readonly: true, class: "form-control"
- = clipboard_button(text: token.token)
- path = impersonation ? revoke_admin_user_impersonation_token_path(token.user, token) : revoke_profile_personal_access_token_path(token)
%td= link_to "Revoke", path, method: :put, class: "btn btn-danger float-right", data: { confirm: "Are you sure you want to revoke this #{type} Token? This action cannot be undone." }
- else
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/_board.html.haml b/app/views/shared/boards/components/_board.html.haml
index e26f5260e5b..c6c5cadc3f5 100644
--- a/app/views/shared/boards/components/_board.html.haml
+++ b/app/views/shared/boards/components/_board.html.haml
@@ -39,14 +39,13 @@
{{ list.issuesSize }}
= render_if_exists "shared/boards/components/list_weight"
- - if can?(current_user, :admin_list, current_board_parent)
- %button.issue-count-badge-add-button.btn.btn-sm.btn-default.ml-1.has-tooltip.js-no-trigger-collapse{ type: "button",
- "@click" => "showNewIssueForm",
- "v-if" => 'list.type !== "closed"',
- "aria-label" => _("New issue"),
- "title" => _("New issue"),
- data: { placement: "top", container: "body" } }
- = icon("plus", class: "js-no-trigger-collapse")
+ %button.issue-count-badge-add-button.btn.btn-sm.btn-default.ml-1.has-tooltip.js-no-trigger-collapse{ type: "button",
+ "@click" => "showNewIssueForm",
+ "v-if" => "isNewIssueShown",
+ "aria-label" => _("New issue"),
+ "title" => _("New issue"),
+ data: { placement: "top", container: "body" } }
+ = icon("plus", class: "js-no-trigger-collapse")
%board-list{ "v-if" => 'list.type !== "blank" && list.type !== "promotion"',
":list" => "list",
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/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
deleted file mode 100644
index c7037335866..00000000000
--- a/app/views/shared/issuable/_filter.html.haml
+++ /dev/null
@@ -1,32 +0,0 @@
-.issues-filters
- .issues-details-filters.row-content-block.second-block
- = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do
- - if params[:search].present?
- = hidden_field_tag :search, params[:search]
- .issues-other-filters
- .filter-item.inline
- - if params[:author_id].present?
- = hidden_field_tag(:author_id, params[:author_id])
- = dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit",
- placeholder: "Search authors", data: { any_user: "Any Author", first_user: current_user&.username, current_user: true, project_id: @project&.id, group_id: @group&.id, selected: params[:author_id], field_name: "author_id", default_label: "Author" } })
-
- .filter-item.inline
- - if params[:assignee_id].present?
- = hidden_field_tag(:assignee_id, params[:assignee_id])
- = dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
- placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: current_user&.username, null_user: true, current_user: true, project_id: @project&.id, group_id: @group&.id, selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } })
-
- .filter-item.inline.milestone-filter
- = render "shared/issuable/milestone_dropdown", selected: finder.milestones.try(:first), name: :milestone_title, show_any: true, show_upcoming: true, show_started: true
-
- .filter-item.inline.labels-filter
- = render "shared/issuable/label_dropdown", selected: selected_labels, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" }
-
- - unless @no_filters_set
- .float-right
- = render 'shared/sort_dropdown'
-
- - has_labels = @labels && @labels.any?
- .row-content-block.second-block.filtered-labels{ class: ("hidden" unless has_labels) }
- - if has_labels
- = render 'shared/labels_row', labels: @labels
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index cb45928d9a5..824bbe3524b 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -1,15 +1,15 @@
- type = local_assigns.fetch(:type)
- board = local_assigns.fetch(:board, nil)
- 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 }
- if type == :boards
#js-multiple-boards-switcher.inline.boards-switcher{ "v-cloak" => true }
= render_if_exists "shared/boards/switcher", board: board
- = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do
+ = form_tag page_filter_path, method: :get, class: 'filter-form js-filter-form' do
- if params[:search].present?
= hidden_field_tag :search, params[:search]
- if @can_bulk_update
@@ -24,7 +24,7 @@
dropdown_class: "filtered-search-history-dropdown",
content_class: "filtered-search-history-dropdown-content",
title: "Recent searches" }) do
- .js-filtered-search-history-dropdown{ data: { full_path: full_path } }
+ .js-filtered-search-history-dropdown{ data: { full_path: search_history_storage_prefix } }
.filtered-search-box-input-container.droplab-dropdown
.scroll-container
%ul.tokens-container.list-unstyled
@@ -61,7 +61,10 @@
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link{ type: 'button' }
- = _('No Assignee')
+ = _('None')
+ %li.filter-dropdown-item{ data: { value: 'any' } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('Any')
%li.divider.droplab-item-ignore
- if current_user
= render 'shared/issuable/user_dropdown_item',
@@ -81,7 +84,7 @@
%li.filter-dropdown-item{ data: { value: 'upcoming' } }
%button.btn.btn-link{ type: 'button' }
= _('Upcoming')
- %li.filter-dropdown-item{ 'data-value' => 'started' }
+ %li.filter-dropdown-item{ data: { value: 'started' } }
%button.btn.btn-link{ type: 'button' }
= _('Started')
%li.divider.droplab-item-ignore
@@ -102,6 +105,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' }
@@ -129,5 +140,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/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 10ffe8dd37f..5295e656ab0 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -24,7 +24,7 @@
.block.milestone
.sidebar-collapsed-icon.has-tooltip{ title: milestone_tooltip_title(issuable.milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
= icon('clock-o', 'aria-hidden': 'true')
- %span.milestone-title
+ %span.milestone-title.collapse-truncated-title
- if issuable.milestone
= issuable.milestone.title
- else
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/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml
index 09ddf732ada..f6c7ca70ebd 100644
--- a/app/views/shared/notifications/_button.html.haml
+++ b/app/views/shared/notifications/_button.html.haml
@@ -9,11 +9,11 @@
%button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
- %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } }
+ %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
= icon('caret-down')
.sr-only Toggle dropdown
- else
- %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), display: 'static' } }
+ %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
= icon("caret-down")
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/u2f/_authenticate.html.haml b/app/views/u2f/_authenticate.html.haml
index 1c788b9a737..979f6862de3 100644
--- a/app/views/u2f/_authenticate.html.haml
+++ b/app/views/u2f/_authenticate.html.haml
@@ -1,18 +1,18 @@
#js-authenticate-u2f
-%a.btn.btn-block.btn-info#js-login-2fa-device{ href: '#' } Sign in via 2FA code
+%a.btn.btn-block.btn-info#js-login-2fa-device{ href: '#' }= _("Sign in via 2FA code")
-# haml-lint:disable InlineJavaScript
%script#js-authenticate-u2f-in-progress{ type: "text/template" }
- %p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.
+ %p= _("Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.")
%script#js-authenticate-u2f-error{ type: "text/template" }
%div
- %p <%= error_message %> (error code: <%= error_code %>)
- %a.btn.btn-block.btn-warning#js-u2f-try-again Try again?
+ %p <%= error_message %> (#{_("error code:")} <%= error_code %>)
+ %a.btn.btn-block.btn-warning#js-u2f-try-again= _("Try again?")
%script#js-authenticate-u2f-authenticated{ type: "text/template" }
%div
- %p We heard back from your U2F device. You have been authenticated.
+ %p= _("We heard back from your U2F device. You have been authenticated.")
= form_tag(new_user_session_path, method: :post, id: 'js-login-u2f-form') do |f|
- resource_params = params[resource_name].presence || params
= hidden_field_tag 'user[remember_me]', resource_params.fetch(:remember_me, 0)
diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml
index 39d4d82a77d..f6724f72307 100644
--- a/app/views/u2f/_register.html.haml
+++ b/app/views/u2f/_register.html.haml
@@ -2,39 +2,39 @@
-# haml-lint:disable InlineJavaScript
%script#js-register-u2f-not-supported{ type: "text/template" }
- %p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).
+ %p= _("Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).")
%script#js-register-u2f-setup{ type: "text/template" }
- if current_user.two_factor_otp_enabled?
.row.append-bottom-10
.col-md-4
- %button#js-setup-u2f-device.btn.btn-info.btn-block Set up new U2F device
+ %button#js-setup-u2f-device.btn.btn-info.btn-block= _("Set up new U2F device")
.col-md-8
- %p Your U2F device needs to be set up. Plug it in (if not already) and click the button on the left.
+ %p= _("Your U2F device needs to be set up. Plug it in (if not already) and click the button on the left.")
- else
.row.append-bottom-10
.col-md-4
- %button#js-setup-u2f-device.btn.btn-info.btn-block{ disabled: true } Set up new U2F device
+ %button#js-setup-u2f-device.btn.btn-info.btn-block{ disabled: true }= _("Set up new U2F device")
.col-md-8
- %p.text-warning You need to register a two-factor authentication app before you can set up a U2F device.
+ %p.text-warning= _("You need to register a two-factor authentication app before you can set up a U2F device.")
%script#js-register-u2f-in-progress{ type: "text/template" }
- %p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.
+ %p= _("Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.")
%script#js-register-u2f-error{ type: "text/template" }
%div
%p
- %span <%= error_message %> (error code: <%= error_code %>)
- %a.btn.btn-warning#js-u2f-try-again Try again?
+ %span <%= error_message %> (#{_("error code:")} <%= error_code %>)
+ %a.btn.btn-warning#js-u2f-try-again= _("Try again?")
%script#js-register-u2f-registered{ type: "text/template" }
.row.append-bottom-10
.col-md-12
- %p Your device was successfully set up! Give it a name and register it with the GitLab server.
+ %p= _("Your device was successfully set up! Give it a name and register it with the GitLab server.")
= form_tag(create_u2f_profile_two_factor_auth_path, method: :post) do
.row.append-bottom-10
.col-md-3
- = text_field_tag 'u2f_registration[name]', nil, class: 'form-control', placeholder: "Pick a name"
+ = text_field_tag 'u2f_registration[name]', nil, class: 'form-control', placeholder: _("Pick a name")
.col-md-3
= hidden_field_tag 'u2f_registration[device_response]', nil, class: 'form-control', required: true, id: "js-device-response"
- = submit_tag "Register U2F device", class: "btn btn-success"
+ = submit_tag _("Register U2F device"), class: "btn btn-success"
diff --git a/app/views/users/_overview.html.haml b/app/views/users/_overview.html.haml
index f8b3754840d..b5bc1180290 100644
--- a/app/views/users/_overview.html.haml
+++ b/app/views/users/_overview.html.haml
@@ -1,32 +1,30 @@
.row
+ .col-12
+ .calendar-block.prepend-top-default.append-bottom-default
+ .user-calendar.d-none.d-sm-block{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: Time.zone.utc_offset } }
+ %h4.center.light
+ = spinner nil, true
+ .user-calendar-activities.d-none.d-sm-block
+.row
.col-md-12.col-lg-6
- .calendar-block
- .content-block.hide-bottom-border
- %h4
- = s_('UserProfile|Activity')
- .user-calendar.d-none.d-sm-block.text-left{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: Time.zone.utc_offset } }
- %h4.center.light
- %i.fa.fa-spinner.fa-spin
- .user-calendar-activities.d-none.d-sm-block
-
- if can?(current_user, :read_cross_project)
.activities-block
- .content-block
- %h5.prepend-top-10
- = s_('UserProfile|Recent contributions')
+ .prepend-top-16
+ .d-flex.align-items-center.border-bottom
+ %h4.flex-grow
+ = s_('UserProfile|Activity')
+ = link_to s_('UserProfile|View all'), user_activity_path, class: "hide js-view-all"
.overview-content-list{ data: { href: user_path } }
.center.light.loading
- %i.fa.fa-spinner.fa-spin
- .prepend-top-10
- = link_to s_('UserProfile|View all'), user_activity_path, class: "hide js-view-all"
+ = spinner nil, true
.col-md-12.col-lg-6
.projects-block
- .content-block
- %h4
- = s_('UserProfile|Personal projects')
+ .prepend-top-16
+ .d-flex.align-items-center.border-bottom
+ %h4.flex-grow
+ = s_('UserProfile|Personal projects')
+ = link_to s_('UserProfile|View all'), user_projects_path, class: "hide js-view-all"
.overview-content-list{ data: { href: user_projects_path } }
.center.light.loading
- %i.fa.fa-spinner.fa-spin
- .prepend-top-10
- = link_to s_('UserProfile|View all'), user_projects_path, class: "hide js-view-all"
+ = spinner nil, true
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index d6c8420b744..d11476738e4 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -124,12 +124,6 @@
- if profile_tab?(:activity)
#activity.tab-pane
- .row-content-block.calendar-block.white.second-block.d-none.d-sm-block
- .user-calendar{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: Time.zone.utc_offset } }
- %h4.center.light
- %i.fa.fa-spinner.fa-spin
- .user-calendar-activities
-
- if can?(current_user, :read_cross_project)
%h4.prepend-top-20
= s_('UserProfile|Most Recent Activity')
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/post_receive.rb b/app/workers/post_receive.rb
index 09a594cdb4e..72a1733a2a8 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -29,15 +29,14 @@ class PostReceive
def process_project_changes(post_received)
changes = []
refs = Set.new
+ @user = post_received.identify
- post_received.changes_refs do |oldrev, newrev, ref|
- @user ||= post_received.identify(newrev)
-
- unless @user
- log("Triggered hook for non-existing user \"#{post_received.identifier}\"")
- return false # rubocop:disable Cop/AvoidReturnFromBlocks
- end
+ unless @user
+ log("Triggered hook for non-existing user \"#{post_received.identifier}\"")
+ return false
+ end
+ post_received.changes_refs do |oldrev, newrev, ref|
if Gitlab::Git.tag_ref?(ref)
GitTagPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute
elsif Gitlab::Git.branch_ref?(ref)
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/bin/web b/bin/web
index ecd0bbd10b0..06ff7c39296 100755
--- a/bin/web
+++ b/bin/web
@@ -3,6 +3,11 @@
cd $(dirname $0)/..
app_root=$(pwd)
+# Switch to experimental PUMA configuration
+if [ -n "${EXPERIMENTAL_PUMA}" ]; then
+ exec bin/web_puma "$@"
+fi
+
unicorn_pidfile="$app_root/tmp/pids/unicorn.pid"
unicorn_config="$app_root/config/unicorn.rb"
unicorn_cmd="bundle exec unicorn_rails -c $unicorn_config -E $RAILS_ENV"
diff --git a/bin/web_puma b/bin/web_puma
new file mode 100755
index 00000000000..178fe84800d
--- /dev/null
+++ b/bin/web_puma
@@ -0,0 +1,63 @@
+#!/bin/sh
+
+set -e
+
+cd $(dirname $0)/..
+app_root=$(pwd)
+
+puma_pidfile="$app_root/tmp/pids/puma.pid"
+puma_config="$app_root/config/puma.rb"
+
+spawn_puma()
+{
+ exec bundle exec puma --config "${puma_config}" "$@"
+}
+
+get_puma_pid()
+{
+ pid=$(cat "${puma_pidfile}")
+ if [ -z "$pid" ] ; then
+ echo "Could not find a PID in $puma_pidfile"
+ exit 1
+ fi
+ echo "${pid}"
+}
+
+start()
+{
+ spawn_puma -d
+}
+
+start_foreground()
+{
+ spawn_puma
+}
+
+stop()
+{
+ get_puma_pid
+ kill -QUIT "$(get_puma_pid)"
+}
+
+reload()
+{
+ kill -USR2 "$(get_puma_pid)"
+}
+
+case "$1" in
+ start)
+ start
+ ;;
+ start_foreground)
+ start_foreground
+ ;;
+ stop)
+ stop
+ ;;
+ reload)
+ reload
+ ;;
+ *)
+ echo "Usage: RAILS_ENV=your_env $0 {start|stop|reload}"
+ ;;
+esac
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/27231-add-license-data-to-projects-endpoint.yml b/changelogs/unreleased/27231-add-license-data-to-projects-endpoint.yml
new file mode 100644
index 00000000000..f5ed6ccf6df
--- /dev/null
+++ b/changelogs/unreleased/27231-add-license-data-to-projects-endpoint.yml
@@ -0,0 +1,5 @@
+---
+title: Add license data to projects endpoint
+merge_request: 21606
+author: J.D. Bean (@jdbean)
+type: added
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/28682-can-merge-branch-before-build-is-started.yml b/changelogs/unreleased/28682-can-merge-branch-before-build-is-started.yml
new file mode 100644
index 00000000000..5ffd93e098f
--- /dev/null
+++ b/changelogs/unreleased/28682-can-merge-branch-before-build-is-started.yml
@@ -0,0 +1,5 @@
+---
+title: Strictly require a pipeline to merge.
+merge_request: 22911
+author:
+type: changed
diff --git a/changelogs/unreleased/34758-create-group-clusters.yml b/changelogs/unreleased/34758-create-group-clusters.yml
new file mode 100644
index 00000000000..50efde3cac3
--- /dev/null
+++ b/changelogs/unreleased/34758-create-group-clusters.yml
@@ -0,0 +1,5 @@
+---
+title: Adds model and migrations to enable group level clusters
+merge_request: 22307
+author:
+type: other
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/40085-add-a-create_merge_request-quick-action.yml b/changelogs/unreleased/40085-add-a-create_merge_request-quick-action.yml
new file mode 100644
index 00000000000..e1614ac7669
--- /dev/null
+++ b/changelogs/unreleased/40085-add-a-create_merge_request-quick-action.yml
@@ -0,0 +1,5 @@
+---
+title: Creates /create_merge_request quickaction
+merge_request: 22485
+author: Jacopo Beschi @jacopo-beschi
+type: added
diff --git a/changelogs/unreleased/40372-prometheus-dashboard-broken-on-firefox.yml b/changelogs/unreleased/40372-prometheus-dashboard-broken-on-firefox.yml
new file mode 100644
index 00000000000..8376fac7abf
--- /dev/null
+++ b/changelogs/unreleased/40372-prometheus-dashboard-broken-on-firefox.yml
@@ -0,0 +1,5 @@
+---
+title: Fix prometheus graphs in firefox
+merge_request: 22400
+author:
+type: fixed
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/41875-allow-pipelines-to-be-deleted-by-project-owners.yml b/changelogs/unreleased/41875-allow-pipelines-to-be-deleted-by-project-owners.yml
new file mode 100644
index 00000000000..0662ff6f523
--- /dev/null
+++ b/changelogs/unreleased/41875-allow-pipelines-to-be-deleted-by-project-owners.yml
@@ -0,0 +1,5 @@
+---
+title: Allow deleting a Pipeline via the API.
+merge_request: 22988
+author:
+type: added
diff --git a/changelogs/unreleased/42790-improve-feedback-for-internal-git-access-checks-timeouts.yml b/changelogs/unreleased/42790-improve-feedback-for-internal-git-access-checks-timeouts.yml
new file mode 100644
index 00000000000..d58d8da3a0e
--- /dev/null
+++ b/changelogs/unreleased/42790-improve-feedback-for-internal-git-access-checks-timeouts.yml
@@ -0,0 +1,5 @@
+---
+title: Adds trace of each access check when git push times out
+merge_request: 22265
+author:
+type: added
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/45068-no-longer-require-a-deploy-to-start-prometheus-monitoring.yml b/changelogs/unreleased/45068-no-longer-require-a-deploy-to-start-prometheus-monitoring.yml
new file mode 100644
index 00000000000..6a305099dde
--- /dev/null
+++ b/changelogs/unreleased/45068-no-longer-require-a-deploy-to-start-prometheus-monitoring.yml
@@ -0,0 +1,5 @@
+---
+title: No longer require a deploy to start Prometheus monitoring
+merge_request: 22401
+author:
+type: changed
diff --git a/changelogs/unreleased/45669-table-in-jobs-on-pipeline.yml b/changelogs/unreleased/45669-table-in-jobs-on-pipeline.yml
new file mode 100644
index 00000000000..97052d01b24
--- /dev/null
+++ b/changelogs/unreleased/45669-table-in-jobs-on-pipeline.yml
@@ -0,0 +1,5 @@
+---
+title: Hide all tables on Pipeline when no Jobs for the Pipeline
+merge_request: 18540
+author: Takuya Noguchi
+type: fixed
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/49479-hide-unmerged-env-perf-stats.yml b/changelogs/unreleased/49479-hide-unmerged-env-perf-stats.yml
new file mode 100644
index 00000000000..5118949f8a3
--- /dev/null
+++ b/changelogs/unreleased/49479-hide-unmerged-env-perf-stats.yml
@@ -0,0 +1,5 @@
+---
+title: Don't show Memory Usage for unmerged MRs
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/49726-upgrade-helm-to-2-11.yml b/changelogs/unreleased/49726-upgrade-helm-to-2-11.yml
new file mode 100644
index 00000000000..dd26af875f5
--- /dev/null
+++ b/changelogs/unreleased/49726-upgrade-helm-to-2-11.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade helm to 2.11.0 and upgrade on every install
+merge_request: 22693
+author:
+type: added
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/51306-fix-inaccessible-dropdown-for-codeless-projects.yml b/changelogs/unreleased/51306-fix-inaccessible-dropdown-for-codeless-projects.yml
new file mode 100644
index 00000000000..13e3bb66430
--- /dev/null
+++ b/changelogs/unreleased/51306-fix-inaccessible-dropdown-for-codeless-projects.yml
@@ -0,0 +1,5 @@
+---
+title: Fix inaccessible dropdown for code-less projects
+merge_request: 22137
+author:
+type: other
diff --git a/changelogs/unreleased/51335-fail-early-when-user-cannot-be-identified.yml b/changelogs/unreleased/51335-fail-early-when-user-cannot-be-identified.yml
new file mode 100644
index 00000000000..a042debc28f
--- /dev/null
+++ b/changelogs/unreleased/51335-fail-early-when-user-cannot-be-identified.yml
@@ -0,0 +1,5 @@
+---
+title: If user was not found, service hooks won't run on post receive background job
+merge_request: 22519
+author:
+type: fixed
diff --git a/changelogs/unreleased/51527-xss-in-mr-source-branch.yml b/changelogs/unreleased/51527-xss-in-mr-source-branch.yml
new file mode 100644
index 00000000000..dae277b6413
--- /dev/null
+++ b/changelogs/unreleased/51527-xss-in-mr-source-branch.yml
@@ -0,0 +1,5 @@
+---
+title: Fix XSS in merge request source branch name
+merge_request:
+author:
+type: security
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/51959-branch-and-tag-name-links.yml b/changelogs/unreleased/51959-branch-and-tag-name-links.yml
new file mode 100644
index 00000000000..64f1522c70d
--- /dev/null
+++ b/changelogs/unreleased/51959-branch-and-tag-name-links.yml
@@ -0,0 +1,5 @@
+---
+title: Chat message push notifications now include links back to GitLab branches
+merge_request: 22651
+author: Tony Castrogiovanni
+type: added
diff --git a/changelogs/unreleased/52115-Link-button-in-markdown-editor-should-recognize-URLs.yml b/changelogs/unreleased/52115-Link-button-in-markdown-editor-should-recognize-URLs.yml
new file mode 100644
index 00000000000..8521335c2ea
--- /dev/null
+++ b/changelogs/unreleased/52115-Link-button-in-markdown-editor-should-recognize-URLs.yml
@@ -0,0 +1,5 @@
+---
+title: Link button in markdown editor recognize URLs
+merge_request: 1983
+author: Johann Hubert Sonntagbauer
+type: changed
diff --git a/changelogs/unreleased/52122-fix-broken-whitespace-button.yml b/changelogs/unreleased/52122-fix-broken-whitespace-button.yml
new file mode 100644
index 00000000000..3f261eb2ac5
--- /dev/null
+++ b/changelogs/unreleased/52122-fix-broken-whitespace-button.yml
@@ -0,0 +1,5 @@
+---
+title: Fix broken "Show whitespace changes" button on MRs.
+merge_request: 22539
+author:
+type: fixed
diff --git a/changelogs/unreleased/52202-consider-moving-isjobstuck-verification-to-backend.yml b/changelogs/unreleased/52202-consider-moving-isjobstuck-verification-to-backend.yml
new file mode 100644
index 00000000000..0efd97d91b8
--- /dev/null
+++ b/changelogs/unreleased/52202-consider-moving-isjobstuck-verification-to-backend.yml
@@ -0,0 +1,5 @@
+---
+title: Renders stuck block when runners are stuck
+merge_request:
+author:
+type: fixed
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/52383-ui-filter-assignee-none-any.yml b/changelogs/unreleased/52383-ui-filter-assignee-none-any.yml
new file mode 100644
index 00000000000..adf153f33ce
--- /dev/null
+++ b/changelogs/unreleased/52383-ui-filter-assignee-none-any.yml
@@ -0,0 +1,5 @@
+---
+title: Add None/Any option for assignee_id in search bar
+merge_request: 22599
+author: Heinrich Lee Yu
+type: added
diff --git a/changelogs/unreleased/52384-api-filter-assignee-none-any.yml b/changelogs/unreleased/52384-api-filter-assignee-none-any.yml
new file mode 100644
index 00000000000..9acec04d946
--- /dev/null
+++ b/changelogs/unreleased/52384-api-filter-assignee-none-any.yml
@@ -0,0 +1,5 @@
+---
+title: Add None/Any option for assignee_id in Issues and Merge Requests API
+merge_request: 22598
+author: Heinrich Lee Yu
+type: added
diff --git a/changelogs/unreleased/52385-search-bar-for-dashboard-list.yml b/changelogs/unreleased/52385-search-bar-for-dashboard-list.yml
new file mode 100644
index 00000000000..a437ae560cb
--- /dev/null
+++ b/changelogs/unreleased/52385-search-bar-for-dashboard-list.yml
@@ -0,0 +1,5 @@
+---
+title: Use search bar for filtering in dashboard issues / MRs
+merge_request: 22641
+author: Heinrich Lee Yu
+type: changed
diff --git a/changelogs/unreleased/52545-guest-create-issue-in-group-board.yml b/changelogs/unreleased/52545-guest-create-issue-in-group-board.yml
new file mode 100644
index 00000000000..5701e44eb32
--- /dev/null
+++ b/changelogs/unreleased/52545-guest-create-issue-in-group-board.yml
@@ -0,0 +1,5 @@
+---
+title: Always show new issue button in boards' Open list
+merge_request: 22557
+author: Heinrich Lee Yu
+type: fixed
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/52712-further-ui-improvements-to-profile-overview-tab.yml b/changelogs/unreleased/52712-further-ui-improvements-to-profile-overview-tab.yml
new file mode 100644
index 00000000000..65aa9323d2e
--- /dev/null
+++ b/changelogs/unreleased/52712-further-ui-improvements-to-profile-overview-tab.yml
@@ -0,0 +1,5 @@
+---
+title: UI improvements to user's profile
+merge_request: 22977
+author:
+type: other
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/52780-stale-pipeline-status-cache-for-_project-after-disabling-pipelines.yml b/changelogs/unreleased/52780-stale-pipeline-status-cache-for-_project-after-disabling-pipelines.yml
new file mode 100644
index 00000000000..7586d7995b7
--- /dev/null
+++ b/changelogs/unreleased/52780-stale-pipeline-status-cache-for-_project-after-disabling-pipelines.yml
@@ -0,0 +1,5 @@
+---
+title: Cache pipeline status per SHA.
+merge_request: 22589
+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/52993-ldap-rename_provider-rake-task-broken.yml b/changelogs/unreleased/52993-ldap-rename_provider-rake-task-broken.yml
new file mode 100644
index 00000000000..ca78f9a392e
--- /dev/null
+++ b/changelogs/unreleased/52993-ldap-rename_provider-rake-task-broken.yml
@@ -0,0 +1,5 @@
+---
+title: Use gitlab_environment for ldap rake task
+merge_request: 22582
+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/53055-combine-date-util-functions.yml b/changelogs/unreleased/53055-combine-date-util-functions.yml
new file mode 100644
index 00000000000..56d4406f1bf
--- /dev/null
+++ b/changelogs/unreleased/53055-combine-date-util-functions.yml
@@ -0,0 +1,5 @@
+---
+title: Combine all datetime library functions into 'datetime_utility.js'
+merge_request: 22570
+author:
+type: other
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/53133-jobs-list.yml b/changelogs/unreleased/53133-jobs-list.yml
new file mode 100644
index 00000000000..2e13edc0e76
--- /dev/null
+++ b/changelogs/unreleased/53133-jobs-list.yml
@@ -0,0 +1,5 @@
+---
+title: Fix stage dropdown not rendering in different languages
+merge_request: 22604
+author:
+type: other
diff --git a/changelogs/unreleased/53155-structured-logs-params-array.yml b/changelogs/unreleased/53155-structured-logs-params-array.yml
new file mode 100644
index 00000000000..4d4f68a5c84
--- /dev/null
+++ b/changelogs/unreleased/53155-structured-logs-params-array.yml
@@ -0,0 +1,5 @@
+---
+title: Use key-value pair arrays for API query parameter logging instead of hashes
+merge_request: 22623
+author:
+type: other
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/53270-remove-mousetrap-rails.yml b/changelogs/unreleased/53270-remove-mousetrap-rails.yml
new file mode 100644
index 00000000000..7214c81d73d
--- /dev/null
+++ b/changelogs/unreleased/53270-remove-mousetrap-rails.yml
@@ -0,0 +1,5 @@
+---
+title: Remove mousetrap-rails gem
+merge_request: 22647
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/53273-update-moment-to-2-22-2.yml b/changelogs/unreleased/53273-update-moment-to-2-22-2.yml
new file mode 100644
index 00000000000..a6b40466927
--- /dev/null
+++ b/changelogs/unreleased/53273-update-moment-to-2-22-2.yml
@@ -0,0 +1,5 @@
+---
+title: Update moment to 2.22.2
+merge_request: 22648
+author: Takuya Noguchi
+type: security
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/53290-incorrect-project-list-order-select-default-label.yml b/changelogs/unreleased/53290-incorrect-project-list-order-select-default-label.yml
new file mode 100644
index 00000000000..d076352a27b
--- /dev/null
+++ b/changelogs/unreleased/53290-incorrect-project-list-order-select-default-label.yml
@@ -0,0 +1,5 @@
+---
+title: Fix default sorting for subgroups and projects list
+merge_request: 23058
+author: Jacopo Beschi @jacopo-beschi
+type: fixed
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/53700-hashed-storagemigration.yml b/changelogs/unreleased/53700-hashed-storagemigration.yml
new file mode 100644
index 00000000000..899012ffd22
--- /dev/null
+++ b/changelogs/unreleased/53700-hashed-storagemigration.yml
@@ -0,0 +1,5 @@
+---
+title: 'Hashed Storage: allow migration to be retried in partially migrated projects'
+merge_request: 23087
+author:
+type: fixed
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/54004-update-asana-to-0-8-1.yml b/changelogs/unreleased/54004-update-asana-to-0-8-1.yml
new file mode 100644
index 00000000000..a47b4f3c4d9
--- /dev/null
+++ b/changelogs/unreleased/54004-update-asana-to-0-8-1.yml
@@ -0,0 +1,5 @@
+---
+title: Update asana to 0.8.1
+merge_request: 23039
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/54010-update-asciidoctor-to-1-5-8.yml b/changelogs/unreleased/54010-update-asciidoctor-to-1-5-8.yml
new file mode 100644
index 00000000000..f0b0aa0ee1c
--- /dev/null
+++ b/changelogs/unreleased/54010-update-asciidoctor-to-1-5-8.yml
@@ -0,0 +1,5 @@
+---
+title: Update asciidoctor to 1.5.8
+merge_request: 23047
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/54015-Markdown-Editor-improve-Cursor-placement.yml b/changelogs/unreleased/54015-Markdown-Editor-improve-Cursor-placement.yml
new file mode 100644
index 00000000000..28e3fae01a9
--- /dev/null
+++ b/changelogs/unreleased/54015-Markdown-Editor-improve-Cursor-placement.yml
@@ -0,0 +1,5 @@
+---
+title: Refine cursor positioning in Markdown Editor for wrap tags
+merge_request: 23085
+author: Johann Hubert Sonntagbauer
+type: changed
diff --git a/changelogs/unreleased/54021-empty-button.yml b/changelogs/unreleased/54021-empty-button.yml
new file mode 100644
index 00000000000..3b03665cf95
--- /dev/null
+++ b/changelogs/unreleased/54021-empty-button.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent empty button being rendered in empty state
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/54093-the-default_value_for-gem-doesn-t-handle-actioncontroller-parameters-correctly.yml b/changelogs/unreleased/54093-the-default_value_for-gem-doesn-t-handle-actioncontroller-parameters-correctly.yml
new file mode 100644
index 00000000000..3d6fd2d065a
--- /dev/null
+++ b/changelogs/unreleased/54093-the-default_value_for-gem-doesn-t-handle-actioncontroller-parameters-correctly.yml
@@ -0,0 +1,7 @@
+---
+title: Fixes an issue where default values from models would override values set in
+ the interface (e.g. users would be set to external even though their emails matches
+ the internal email address pattern)
+merge_request: 23114
+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/ac-post-merge-pipeline.yml b/changelogs/unreleased/ac-post-merge-pipeline.yml
new file mode 100644
index 00000000000..08322c9cb8a
--- /dev/null
+++ b/changelogs/unreleased/ac-post-merge-pipeline.yml
@@ -0,0 +1,5 @@
+---
+title: Show post-merge pipeline in merge request page
+merge_request: 22292
+author:
+type: added
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-failure-reason-for-execution-timeout.yml b/changelogs/unreleased/add-failure-reason-for-execution-timeout.yml
new file mode 100644
index 00000000000..c8488cbf200
--- /dev/null
+++ b/changelogs/unreleased/add-failure-reason-for-execution-timeout.yml
@@ -0,0 +1,5 @@
+---
+title: Add failure reason for execution timeout
+merge_request: 22224
+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/added-glob-for-ci-changes-detection.yml b/changelogs/unreleased/added-glob-for-ci-changes-detection.yml
new file mode 100644
index 00000000000..887c6ef0346
--- /dev/null
+++ b/changelogs/unreleased/added-glob-for-ci-changes-detection.yml
@@ -0,0 +1,5 @@
+---
+title: Added glob for CI changes detection
+merge_request: 23128
+author: Kirill Zaitsev
+type: added
diff --git a/changelogs/unreleased/an-gitaly-version-0-133-0.yml b/changelogs/unreleased/an-gitaly-version-0-133-0.yml
new file mode 100644
index 00000000000..4f3943ceacb
--- /dev/null
+++ b/changelogs/unreleased/an-gitaly-version-0-133-0.yml
@@ -0,0 +1,5 @@
+---
+title: Updated Gitaly to v0.133.0
+merge_request: 23148
+author:
+type: other
diff --git a/changelogs/unreleased/an-multithreading.yml b/changelogs/unreleased/an-multithreading.yml
new file mode 100644
index 00000000000..fca847e6ea4
--- /dev/null
+++ b/changelogs/unreleased/an-multithreading.yml
@@ -0,0 +1,5 @@
+---
+title: Experimental support for running Puma multithreaded web-server
+merge_request: 22372
+author:
+type: performance
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-support-for-group-security-dashboard.yml b/changelogs/unreleased/auto-devops-support-for-group-security-dashboard.yml
new file mode 100644
index 00000000000..7fb11f24902
--- /dev/null
+++ b/changelogs/unreleased/auto-devops-support-for-group-security-dashboard.yml
@@ -0,0 +1,5 @@
+---
+title: Auto DevOps support for Group Security Dashboard
+merge_request: 23165
+author:
+type: fixed
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/avoid-lock-when-introduce-new-failure-reason.yml b/changelogs/unreleased/avoid-lock-when-introduce-new-failure-reason.yml
new file mode 100644
index 00000000000..30b9ae032d4
--- /dev/null
+++ b/changelogs/unreleased/avoid-lock-when-introduce-new-failure-reason.yml
@@ -0,0 +1,5 @@
+---
+title: Support backward compatibility when introduce new failure reason
+merge_request: 22566
+author:
+type: changed
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/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/bvl-preload-user-status-for-events.yml b/changelogs/unreleased/bvl-preload-user-status-for-events.yml
new file mode 100644
index 00000000000..e13b19b19c1
--- /dev/null
+++ b/changelogs/unreleased/bvl-preload-user-status-for-events.yml
@@ -0,0 +1,5 @@
+---
+title: Show user status for label events in system notes
+merge_request: 22609
+author:
+type: fixed
diff --git a/changelogs/unreleased/bw-automatically-navigate-to-last-board-visited.yml b/changelogs/unreleased/bw-automatically-navigate-to-last-board-visited.yml
new file mode 100644
index 00000000000..836b9aa21c5
--- /dev/null
+++ b/changelogs/unreleased/bw-automatically-navigate-to-last-board-visited.yml
@@ -0,0 +1,5 @@
+---
+title: Automatically navigate to last board visited
+merge_request: 22430
+author:
+type: changed
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/ce-53347_fix_impersonation_tokens.yml b/changelogs/unreleased/ce-53347_fix_impersonation_tokens.yml
new file mode 100644
index 00000000000..6cc743d6f3a
--- /dev/null
+++ b/changelogs/unreleased/ce-53347_fix_impersonation_tokens.yml
@@ -0,0 +1,5 @@
+---
+title: Display impersonation token value only after creation
+merge_request: 22916
+author:
+type: fixed
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-default-value-status-deployments.yml b/changelogs/unreleased/drop-default-value-status-deployments.yml
new file mode 100644
index 00000000000..fdb826a0507
--- /dev/null
+++ b/changelogs/unreleased/drop-default-value-status-deployments.yml
@@ -0,0 +1,5 @@
+---
+title: Drop default value on status column in deployments table
+merge_request: 22971
+author:
+type: other
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-53298.yml b/changelogs/unreleased/fix-53298.yml
new file mode 100644
index 00000000000..f0bf5470dc8
--- /dev/null
+++ b/changelogs/unreleased/fix-53298.yml
@@ -0,0 +1,5 @@
+---
+title: 'Fix #53298: JupyterHub restarts should work without errors'
+merge_request: 22671
+author: Amit Rathi
+type: fixed
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-not-render-emoji.yml b/changelogs/unreleased/fix-not-render-emoji.yml
new file mode 100644
index 00000000000..857b97004f0
--- /dev/null
+++ b/changelogs/unreleased/fix-not-render-emoji.yml
@@ -0,0 +1,5 @@
+---
+title: Fix not render emoji in filter dropdown
+merge_request: 23112
+author: Hiroyuki Sato
+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-even-even-more.yml b/changelogs/unreleased/frozen-string-lib-gitlab-even-even-more.yml
new file mode 100644
index 00000000000..e718d716647
--- /dev/null
+++ b/changelogs/unreleased/frozen-string-lib-gitlab-even-even-more.yml
@@ -0,0 +1,5 @@
+---
+title: Enable even more frozen string for lib/gitlab
+merge_request:
+author: gfyoung
+type: performance
diff --git a/changelogs/unreleased/frozen-string-lib-gitlab-even-more.yml b/changelogs/unreleased/frozen-string-lib-gitlab-even-more.yml
new file mode 100644
index 00000000000..cfbc4ced635
--- /dev/null
+++ b/changelogs/unreleased/frozen-string-lib-gitlab-even-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/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-fix-typo-in-notebook-props.yml b/changelogs/unreleased/gt-fix-typo-in-notebook-props.yml
new file mode 100644
index 00000000000..60603905a2d
--- /dev/null
+++ b/changelogs/unreleased/gt-fix-typo-in-notebook-props.yml
@@ -0,0 +1,5 @@
+---
+title: Fix typo in notebook props
+merge_request: 23103
+author: George Tsiolis
+type: other
diff --git a/changelogs/unreleased/gt-fix-typos-in-lib.yml b/changelogs/unreleased/gt-fix-typos-in-lib.yml
new file mode 100644
index 00000000000..32ccd03b063
--- /dev/null
+++ b/changelogs/unreleased/gt-fix-typos-in-lib.yml
@@ -0,0 +1,5 @@
+---
+title: Fix typos in lib
+merge_request: 23106
+author: George Tsiolis
+type: other
diff --git a/changelogs/unreleased/gt-remove-unused-project-method.yml b/changelogs/unreleased/gt-remove-unused-project-method.yml
new file mode 100644
index 00000000000..2d60c2fe423
--- /dev/null
+++ b/changelogs/unreleased/gt-remove-unused-project-method.yml
@@ -0,0 +1,5 @@
+---
+title: Remove unused project method
+merge_request: 54103
+author: George Tsiolis
+type: other
diff --git a/changelogs/unreleased/gt-rename-diffs-store-variable.yml b/changelogs/unreleased/gt-rename-diffs-store-variable.yml
new file mode 100644
index 00000000000..0aed49f3d60
--- /dev/null
+++ b/changelogs/unreleased/gt-rename-diffs-store-variable.yml
@@ -0,0 +1,5 @@
+---
+title: Rename diffs store variable
+merge_request: 23123
+author: George Tsiolis
+type: other
diff --git a/changelogs/unreleased/gt-truncate-milestone-title-on-collapsed-sidebar.yml b/changelogs/unreleased/gt-truncate-milestone-title-on-collapsed-sidebar.yml
new file mode 100644
index 00000000000..ca3b99e73ab
--- /dev/null
+++ b/changelogs/unreleased/gt-truncate-milestone-title-on-collapsed-sidebar.yml
@@ -0,0 +1,5 @@
+---
+title: Truncate milestone title on collapsed sidebar
+merge_request: 22624
+author: George Tsiolis
+type: changed
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/jramsay-42673-commit-tooltip.yml b/changelogs/unreleased/jramsay-42673-commit-tooltip.yml
new file mode 100644
index 00000000000..083cd1a54a0
--- /dev/null
+++ b/changelogs/unreleased/jramsay-42673-commit-tooltip.yml
@@ -0,0 +1,5 @@
+---
+title: Add commit message to commit tree anchor title
+merge_request: 22585
+author:
+type: fixed
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/pl-uprade-prometheus-alertmanager.yml b/changelogs/unreleased/pl-uprade-prometheus-alertmanager.yml
new file mode 100644
index 00000000000..d0c8ed8001d
--- /dev/null
+++ b/changelogs/unreleased/pl-uprade-prometheus-alertmanager.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade Prometheus to 2.4.3 and Alertmanager to 0.15.2
+merge_request: 22600
+author:
+type: other
diff --git a/changelogs/unreleased/rails5-active-record-class-value.yml b/changelogs/unreleased/rails5-active-record-class-value.yml
new file mode 100644
index 00000000000..9f9fdf10cd1
--- /dev/null
+++ b/changelogs/unreleased/rails5-active-record-class-value.yml
@@ -0,0 +1,5 @@
+---
+title: 'Rails5: Passing a class as a value in an Active Record query is deprecated'
+merge_request: 23164
+author: Jasper Maes
+type: other
diff --git a/changelogs/unreleased/rails5-deprecated-uniq.yml b/changelogs/unreleased/rails5-deprecated-uniq.yml
new file mode 100644
index 00000000000..69a169100f0
--- /dev/null
+++ b/changelogs/unreleased/rails5-deprecated-uniq.yml
@@ -0,0 +1,5 @@
+---
+title: Replace deprecated uniq on a Relation with distinct
+merge_request: 22625
+author: Jasper Maes
+type: other
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/ravlen-rename-secret-variables-in-codebase.yml b/changelogs/unreleased/ravlen-rename-secret-variables-in-codebase.yml
new file mode 100644
index 00000000000..211d51a3d43
--- /dev/null
+++ b/changelogs/unreleased/ravlen-rename-secret-variables-in-codebase.yml
@@ -0,0 +1,5 @@
+---
+title: "Secret Variables renamed to CI Variables in the codebase, to match UX"
+merge_request: 22414
+author: Marcel Amirault @ravlen
+type: changed \ No newline at end of file
diff --git a/changelogs/unreleased/redact-links-dev.yml b/changelogs/unreleased/redact-links-dev.yml
new file mode 100644
index 00000000000..338e7965465
--- /dev/null
+++ b/changelogs/unreleased/redact-links-dev.yml
@@ -0,0 +1,5 @@
+---
+title: Redact personal tokens in unsubscribe links.
+merge_request:
+author:
+type: security
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/rz_fix_milestone_count.yml b/changelogs/unreleased/rz_fix_milestone_count.yml
new file mode 100644
index 00000000000..1013b88e0bc
--- /dev/null
+++ b/changelogs/unreleased/rz_fix_milestone_count.yml
@@ -0,0 +1,5 @@
+---
+title: Fixing count on Milestones
+merge_request: 21446
+author:
+type: fixed
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-2717-fix-issue-title-xss.yml b/changelogs/unreleased/security-2717-fix-issue-title-xss.yml
new file mode 100644
index 00000000000..f2e638e5ab5
--- /dev/null
+++ b/changelogs/unreleased/security-2717-fix-issue-title-xss.yml
@@ -0,0 +1,5 @@
+---
+title: Escape entity title while autocomplete template rendering to prevent XSS
+merge_request: 2556
+author:
+type: security
diff --git a/changelogs/unreleased/security-2717-xss-username-autocomplete.yml b/changelogs/unreleased/security-2717-xss-username-autocomplete.yml
new file mode 100644
index 00000000000..d9b1015eeb4
--- /dev/null
+++ b/changelogs/unreleased/security-2717-xss-username-autocomplete.yml
@@ -0,0 +1,5 @@
+---
+title: Escape user fullname while rendering autocomplete template to prevent XSS
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-51113-hash_personal_access_tokens.yml b/changelogs/unreleased/security-51113-hash_personal_access_tokens.yml
new file mode 100644
index 00000000000..4cebe814148
--- /dev/null
+++ b/changelogs/unreleased/security-51113-hash_personal_access_tokens.yml
@@ -0,0 +1,5 @@
+---
+title: Persist only SHA digest of PersonalAccessToken#token
+merge_request:
+author:
+type: security
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-gems-security.yml b/changelogs/unreleased/sh-bump-gems-security.yml
new file mode 100644
index 00000000000..06489f6f979
--- /dev/null
+++ b/changelogs/unreleased/sh-bump-gems-security.yml
@@ -0,0 +1,5 @@
+---
+title: Bump nokogiri, loofah, and rack gems for security updates
+merge_request: 23204
+author:
+type: security
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-hipchat-ssrf.yml b/changelogs/unreleased/sh-fix-hipchat-ssrf.yml
new file mode 100644
index 00000000000..cdc95a34fcf
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-hipchat-ssrf.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent SSRF attacks in HipChat integration
+merge_request:
+author:
+type: security
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-issue-53153.yml b/changelogs/unreleased/sh-fix-issue-53153.yml
new file mode 100644
index 00000000000..ee51631f959
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-issue-53153.yml
@@ -0,0 +1,5 @@
+---
+title: Fix extra merge request versions created from forked merge requests
+merge_request: 22611
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-issue-53783-ce.yml b/changelogs/unreleased/sh-fix-issue-53783-ce.yml
new file mode 100644
index 00000000000..10be1d81768
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-issue-53783-ce.yml
@@ -0,0 +1,5 @@
+---
+title: Fix enabling project deploy key for admins
+merge_request: 23043
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-issue-54189.yml b/changelogs/unreleased/sh-fix-issue-54189.yml
new file mode 100644
index 00000000000..eee743aa5d9
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-issue-54189.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent templated services from being imported
+merge_request:
+author:
+type: security
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-fix-wiki-security-issue-53072.yml b/changelogs/unreleased/sh-fix-wiki-security-issue-53072.yml
new file mode 100644
index 00000000000..ac6ab7cc3f4
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-wiki-security-issue-53072.yml
@@ -0,0 +1,5 @@
+---
+title: Validate Wiki attachments are valid temporary files
+merge_request:
+author:
+type: security
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/sh-remove-local-sidekiq-admin-check.yml b/changelogs/unreleased/sh-remove-local-sidekiq-admin-check.yml
new file mode 100644
index 00000000000..3ec15908fc7
--- /dev/null
+++ b/changelogs/unreleased/sh-remove-local-sidekiq-admin-check.yml
@@ -0,0 +1,5 @@
+---
+title: Remove display of local Sidekiq process in /admin/sidekiq
+merge_request: 23118
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-use-nakayoshi-fork.yml b/changelogs/unreleased/sh-use-nakayoshi-fork.yml
new file mode 100644
index 00000000000..5977d9b0974
--- /dev/null
+++ b/changelogs/unreleased/sh-use-nakayoshi-fork.yml
@@ -0,0 +1,5 @@
+---
+title: Improve memory performance by reducing dirty pages after fork()
+merge_request: 23169
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-use-nokogiri-xml-backend.yml b/changelogs/unreleased/sh-use-nokogiri-xml-backend.yml
new file mode 100644
index 00000000000..6a82e32c416
--- /dev/null
+++ b/changelogs/unreleased/sh-use-nokogiri-xml-backend.yml
@@ -0,0 +1,5 @@
+---
+title: Use Nokogiri as the ActiveSupport XML backend
+merge_request: 23136
+author:
+type: performance
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-lfs-objects-file-store.yml b/changelogs/unreleased/tc-index-lfs-objects-file-store.yml
new file mode 100644
index 00000000000..90e80cb1ef1
--- /dev/null
+++ b/changelogs/unreleased/tc-index-lfs-objects-file-store.yml
@@ -0,0 +1,5 @@
+---
+title: Enhance performance of counting local LFS objects
+merge_request: 22143
+author:
+type: performance
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/validate-foreign-keys-being-indexed.yml b/changelogs/unreleased/validate-foreign-keys-being-indexed.yml
new file mode 100644
index 00000000000..6608a93c08f
--- /dev/null
+++ b/changelogs/unreleased/validate-foreign-keys-being-indexed.yml
@@ -0,0 +1,5 @@
+---
+title: Validate foreign keys being created and indexed for column with _id
+merge_request: 22808
+author:
+type: performance
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/winh-pipeline-actions-dynamic-timer.yml b/changelogs/unreleased/winh-pipeline-actions-dynamic-timer.yml
new file mode 100644
index 00000000000..4ea1d3f8256
--- /dev/null
+++ b/changelogs/unreleased/winh-pipeline-actions-dynamic-timer.yml
@@ -0,0 +1,5 @@
+---
+title: Add dynamic timer for delayed jobs in pipelines list
+merge_request: 22621
+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-improve-gitaly-pb.yml b/changelogs/unreleased/zj-improve-gitaly-pb.yml
new file mode 100644
index 00000000000..506a0303d8a
--- /dev/null
+++ b/changelogs/unreleased/zj-improve-gitaly-pb.yml
@@ -0,0 +1,5 @@
+---
+title: Show what RPC is called in the performance bar
+merge_request: 23140
+author:
+type: other
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..921baa5d617 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"
@@ -158,6 +158,9 @@ module Gitlab
config.action_view.sanitized_allowed_protocols = %w(smb)
+ # Nokogiri is significantly faster and uses less memory than REXML
+ ActiveSupport::XmlMini.backend = 'Nokogiri'
+
# This middleware needs to precede ActiveRecord::QueryCache and other middlewares that
# connect to the database.
config.middleware.insert_after Rails::Rack::Logger, ::Gitlab::Middleware::BasicHealthCheck
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..84d47bd52ad 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
@@ -544,13 +544,6 @@
:why: https://github.com/xtuc/webassemblyjs/blob/master/LICENSE
:versions: []
:when: 2018-06-08 05:30:56.764116000 Z
-- - :license
- - "@gitlab-org/gitlab-ui"
- - MIT
- - :who: Clement Ho
- :why: Our own library
- :versions: []
- :when: 2018-07-17 21:02:54.529227000 Z
- - :approve
- lz-string
- :who: Phil Hughes
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 a4db125f831..09e21b2c6f2 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/7_prometheus_metrics.rb b/config/initializers/7_prometheus_metrics.rb
index 146c4b1e024..8052880cc3d 100644
--- a/config/initializers/7_prometheus_metrics.rb
+++ b/config/initializers/7_prometheus_metrics.rb
@@ -26,9 +26,25 @@ Sidekiq.configure_server do |config|
end
if !Rails.env.test? && Gitlab::Metrics.prometheus_metrics_enabled?
- unless Sidekiq.server?
- Gitlab::Metrics::Samplers::UnicornSampler.initialize_instance(Settings.monitoring.unicorn_sampler_interval).start
+ Gitlab::Cluster::LifecycleEvents.on_worker_start do
+ defined?(::Prometheus::Client.reinitialize_on_pid_change) && Prometheus::Client.reinitialize_on_pid_change
+
+ unless Sidekiq.server?
+ Gitlab::Metrics::Samplers::UnicornSampler.initialize_instance(Settings.monitoring.unicorn_sampler_interval).start
+ end
+
+ Gitlab::Metrics::Samplers::RubySampler.initialize_instance(Settings.monitoring.ruby_sampler_interval).start
end
+end
- Gitlab::Metrics::Samplers::RubySampler.initialize_instance(Settings.monitoring.ruby_sampler_interval).start
+Gitlab::Cluster::LifecycleEvents.on_master_restart do
+ # The following is necessary to ensure stale Prometheus metrics don't
+ # accumulate over time. It needs to be done in this hook as opposed to
+ # inside an init script to ensure metrics files aren't deleted after new
+ # unicorn workers start after a SIGUSR2 is received.
+ prometheus_multiproc_dir = ENV['prometheus_multiproc_dir']
+ if prometheus_multiproc_dir
+ old_metrics = Dir[File.join(prometheus_multiproc_dir, '*.db')]
+ FileUtils.rm_rf(old_metrics)
+ end
end
diff --git a/config/initializers/8_metrics.rb b/config/initializers/8_metrics.rb
index eccf82ab8dc..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'
@@ -158,7 +162,9 @@ if Gitlab::Metrics.enabled? && !Rails.env.test?
GC::Profiler.enable
- Gitlab::Metrics::Samplers::InfluxSampler.initialize_instance.start
+ Gitlab::Cluster::LifecycleEvents.on_worker_start do
+ Gitlab::Metrics::Samplers::InfluxSampler.initialize_instance.start
+ end
module TrackNewRedisConnections
def connect(*args)
diff --git a/config/initializers/active_record_lifecycle.rb b/config/initializers/active_record_lifecycle.rb
new file mode 100644
index 00000000000..7fa37121efc
--- /dev/null
+++ b/config/initializers/active_record_lifecycle.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+# Don't handle sidekiq configuration as it
+# has its own special active record configuration here
+if defined?(ActiveRecord::Base) && !Sidekiq.server?
+ Gitlab::Cluster::LifecycleEvents.on_worker_start do
+ ActiveSupport.on_load(:active_record) do
+ ActiveRecord::Base.establish_connection
+
+ Rails.logger.debug("ActiveRecord connection established")
+ end
+ end
+end
+
+if defined?(ActiveRecord::Base)
+ Gitlab::Cluster::LifecycleEvents.on_before_fork do
+ # the following is highly recommended for Rails + "preload_app true"
+ # as there's no need for the master process to hold a connection
+ ActiveRecord::Base.connection.disconnect!
+
+ Rails.logger.debug("ActiveRecord connection disconnected")
+ end
+end
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/hipchat_client_patch.rb b/config/initializers/hipchat_client_patch.rb
new file mode 100644
index 00000000000..aec265312bb
--- /dev/null
+++ b/config/initializers/hipchat_client_patch.rb
@@ -0,0 +1,14 @@
+# This monkey patches the HTTParty used in https://github.com/hipchat/hipchat-rb.
+module HipChat
+ class Client
+ connection_adapter ::Gitlab::ProxyHTTPConnectionAdapter
+ end
+
+ class Room
+ connection_adapter ::Gitlab::ProxyHTTPConnectionAdapter
+ end
+
+ class User
+ connection_adapter ::Gitlab::ProxyHTTPConnectionAdapter
+ end
+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/macos.rb b/config/initializers/macos.rb
new file mode 100644
index 00000000000..f410af6ed47
--- /dev/null
+++ b/config/initializers/macos.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+if /darwin/ =~ RUBY_PLATFORM
+ Gitlab::Cluster::LifecycleEvents.on_before_fork do
+ require 'fiddle'
+
+ # Dynamically load Foundation.framework, ~implicitly~ initialising
+ # the Objective-C runtime before any forking happens in Unicorn
+ #
+ # From https://bugs.ruby-lang.org/issues/14009
+ Fiddle.dlopen '/System/Library/Frameworks/Foundation.framework/Foundation'
+ 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/initializers/rbtrace.rb b/config/initializers/rbtrace.rb
new file mode 100644
index 00000000000..6a1b71bf4bd
--- /dev/null
+++ b/config/initializers/rbtrace.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+if ENV['ENABLE_RBTRACE']
+ Gitlab::Cluster::LifecycleEvents.on_worker_start do
+ # Unicorn clears out signals before it forks, so rbtrace won't work
+ # unless it is enabled after the fork.
+ require 'rbtrace'
+ end
+end
diff --git a/config/initializers/routing_draw.rb b/config/initializers/routing_draw.rb
index 25003cf0239..f0f74954eef 100644
--- a/config/initializers/routing_draw.rb
+++ b/config/initializers/routing_draw.rb
@@ -1,7 +1,3 @@
# Adds draw method into Rails routing
-# It allows us to keep routing splitted into files
-class ActionDispatch::Routing::Mapper
- def draw(routes_name)
- instance_eval(File.read(Rails.root.join("config/routes/#{routes_name}.rb")))
- end
-end
+# It allows us to keep routing split into files
+ActionDispatch::Routing::Mapper.prepend Gitlab::Patch::DrawRoute
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index bc6b7aed6aa..565efc858d1 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -14,8 +14,6 @@ Sidekiq.default_worker_options = { retry: 3 }
enable_json_logs = Gitlab.config.sidekiq.log_format == 'json'
Sidekiq.configure_server do |config|
- require 'rbtrace' if ENV['ENABLE_RBTRACE']
-
config.redis = queues_config_hash
config.server_middleware do |chain|
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/puma.example.development.rb b/config/puma.example.development.rb
new file mode 100644
index 00000000000..490c940077a
--- /dev/null
+++ b/config/puma.example.development.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+# -----------------------------------------------------------------------
+# This file is used by the GDK to generate a default config/puma.rb file
+# Note that `/home/git` will be substituted for the actual GDK root
+# directory when this file is generated
+# -----------------------------------------------------------------------
+
+# Load "path" as a rackup file.
+#
+# The default is "config.ru".
+#
+rackup 'config.ru'
+pidfile '/home/git/gitlab/tmp/pids/puma.pid'
+state_path '/home/git/gitlab/tmp/pids/puma.state'
+
+stdout_redirect '/home/git/gitlab/log/puma.stdout.log',
+ '/home/git/gitlab/log/puma.stderr.log',
+ true
+
+# Configure "min" to be the minimum number of threads to use to answer
+# requests and "max" the maximum.
+#
+# The default is "0, 16".
+#
+threads 1, 4
+
+# By default, workers accept all requests and queue them to pass to handlers.
+# When false, workers accept the number of simultaneous requests configured.
+#
+# Queueing requests generally improves performance, but can cause deadlocks if
+# the app is waiting on a request to itself. See https://github.com/puma/puma/issues/612
+#
+# When set to false this may require a reverse proxy to handle slow clients and
+# queue requests before they reach puma. This is due to disabling HTTP keepalive
+queue_requests false
+
+# Bind the server to "url". "tcp://", "unix://" and "ssl://" are the only
+# accepted protocols.
+bind 'unix:///home/git/gitlab.socket'
+
+workers 2
+
+require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events"
+require_relative "/home/git/gitlab/lib/gitlab/cluster/puma_worker_killer_initializer"
+
+on_restart do
+ # Signal application hooks that we're about to restart
+ Gitlab::Cluster::LifecycleEvents.do_master_restart
+end
+
+before_fork do
+ # Signal to the puma killer
+ Gitlab::Cluster::PumaWorkerKillerInitializer.start @config.options unless ENV['DISABLE_PUMA_WORKER_KILLER']
+
+ # Signal application hooks that we're about to fork
+ Gitlab::Cluster::LifecycleEvents.do_before_fork
+end
+
+Gitlab::Cluster::LifecycleEvents.set_puma_options @config.options
+on_worker_boot do
+ # Signal application hooks of worker start
+ Gitlab::Cluster::LifecycleEvents.do_worker_start
+end
+
+# Preload the application before starting the workers; this conflicts with
+# phased restart feature. (off by default)
+
+preload_app!
+
+tag 'gitlab-puma-worker'
+
+# Verifies that all workers have checked in to the master process within
+# the given timeout. If not the worker process will be restarted. Default
+# value is 60 seconds.
+#
+worker_timeout 60
diff --git a/config/routes.rb b/config/routes.rb
index c081ca9672a..484e05114be 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -34,6 +34,8 @@ Rails.application.routes.draw do
match '*all', via: [:get, :post], to: proc { [404, {}, ['']] }
end
+ draw :oauth
+
use_doorkeeper_openid_connect
# Autocomplete
@@ -78,7 +80,32 @@ Rails.application.routes.draw do
get 'ide' => 'ide#index'
get 'ide/*vueroute' => 'ide#index', format: false
+ 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/admin.rb b/config/routes/admin.rb
index fb29c4748c1..af333bdc748 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -71,6 +71,7 @@ namespace :admin do
resource :logs, only: [:show]
resource :health_check, controller: 'health_check', only: [:show]
resource :background_jobs, controller: 'background_jobs', only: [:show]
+
resource :system_info, controller: 'system_info', only: [:show]
resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ }
@@ -104,6 +105,7 @@ namespace :admin do
resource :application_settings, only: [:show, :update] do
resources :services, only: [:index, :edit, :update]
+
get :usage_data
put :reset_registration_token
put :reset_health_check_token
diff --git a/config/routes/group.rb b/config/routes/group.rb
index 602bbe837cf..a0aeebe4b91 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
resources :groups, only: [:index, :new, :create] do
post :preview_markdown
end
@@ -51,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
@@ -63,7 +67,6 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
end
end
- # On CE only index and show actions are needed
resources :boards, only: [:index, :show]
resources :runners, only: [:index, :edit, :update, :destroy, :show] do
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 9cbd5b644f6..3f1ad90dfca 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -178,6 +178,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resource :mirror, only: [:show, :update] do
member do
+ get :ssh_host_keys, constraints: { format: :json }
post :update_now
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/config/unicorn.rb.example b/config/unicorn.rb.example
index e06cce3e97a..4637eb8bc6e 100644
--- a/config/unicorn.rb.example
+++ b/config/unicorn.rb.example
@@ -81,22 +81,16 @@ preload_app true
# fast LAN.
check_client_connection false
+require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events"
+
before_exec do |server|
- # The following is necessary to ensure stale Prometheus metrics don't
- # accumulate over time. It needs to be done in this hook as opposed to
- # inside an init script to ensure metrics files aren't deleted after new
- # unicorn workers start after a SIGUSR2 is received.
- if ENV['prometheus_multiproc_dir']
- old_metrics = Dir[File.join(ENV['prometheus_multiproc_dir'], '*.db')]
- FileUtils.rm_rf(old_metrics)
- end
+ # Signal application hooks that we're about to restart
+ Gitlab::Cluster::LifecycleEvents.do_master_restart
end
before_fork do |server, worker|
- # the following is highly recommended for Rails + "preload_app true"
- # as there's no need for the master process to hold a connection
- defined?(ActiveRecord::Base) &&
- ActiveRecord::Base.connection.disconnect!
+ # Signal application hooks that we're about to fork
+ Gitlab::Cluster::LifecycleEvents.do_before_fork
# The following is only recommended for memory/DB-constrained
# installations. It is not needed if your system can house
@@ -124,25 +118,10 @@ before_fork do |server, worker|
end
after_fork do |server, worker|
- # Unicorn clears out signals before it forks, so rbtrace won't work
- # unless it is enabled after the fork.
- require 'rbtrace' if ENV['ENABLE_RBTRACE']
+ # Signal application hooks of worker start
+ Gitlab::Cluster::LifecycleEvents.do_worker_start
# per-process listener ports for debugging/admin/migrations
# addr = "127.0.0.1:#{9293 + worker.nr}"
# server.listen(addr, :tries => -1, :delay => 5, :tcp_nopush => true)
-
- # the following is *required* for Rails + "preload_app true",
- defined?(ActiveRecord::Base) &&
- ActiveRecord::Base.establish_connection
-
- # reset prometheus client, this will cause any opened metrics files to be closed
- defined?(::Prometheus::Client.reinitialize_on_pid_change) &&
- Prometheus::Client.reinitialize_on_pid_change
-
- # if preload_app is true, then you may also want to check and
- # restart any other shared sockets/descriptors such as Memcached,
- # and Redis. TokyoCabinet file handles are safe to reuse
- # between any number of forked children (assuming your kernel
- # correctly implements pread()/pwrite() system calls)
end
diff --git a/config/unicorn.rb.example.development b/config/unicorn.rb.example.development
index f31df66015a..f7541bb9d55 100644
--- a/config/unicorn.rb.example.development
+++ b/config/unicorn.rb.example.development
@@ -1,32 +1,61 @@
+# frozen_string_literal: true
+
+# -------------------------------------------------------------------------
+# This file is used by the GDK to generate a default config/unicorn.rb file
+# Note that `/home/git` will be substituted for the actual GDK root
+# directory when this file is generated
+# -------------------------------------------------------------------------
+
worker_processes 2
timeout 60
+listen '/home/git/gitlab.socket'
+
preload_app true
check_client_connection false
+require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events"
+
+before_exec do |server|
+ # Signal application hooks that we're about to restart
+ Gitlab::Cluster::LifecycleEvents.do_master_restart
+end
+
before_fork do |server, worker|
- # the following is highly recommended for Rails + "preload_app true"
- # as there's no need for the master process to hold a connection
- defined?(ActiveRecord::Base) &&
- ActiveRecord::Base.connection.disconnect!
-
- if /darwin/ =~ RUBY_PLATFORM
- require 'fiddle'
-
- # Dynamically load Foundation.framework, ~implicitly~ initialising
- # the Objective-C runtime before any forking happens in Unicorn
- #
- # From https://bugs.ruby-lang.org/issues/14009
- Fiddle.dlopen '/System/Library/Frameworks/Foundation.framework/Foundation'
+ # Signal application hooks that we're about to fork
+ Gitlab::Cluster::LifecycleEvents.do_before_fork
+
+ # The following is only recommended for memory/DB-constrained
+ # installations. It is not needed if your system can house
+ # twice as many worker_processes as you have configured.
+ #
+ # This allows a new master process to incrementally
+ # phase out the old master process with SIGTTOU to avoid a
+ # thundering herd (especially in the "preload_app false" case)
+ # when doing a transparent upgrade. The last worker spawned
+ # will then kill off the old master process with a SIGQUIT.
+ old_pid = "#{server.config[:pid]}.oldbin"
+ if old_pid != server.pid
+ begin
+ sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
+ Process.kill(sig, File.read(old_pid).to_i)
+ rescue Errno::ENOENT, Errno::ESRCH
+ end
end
+ #
+ # Throttle the master from forking too quickly by sleeping. Due
+ # to the implementation of standard Unix signal handlers, this
+ # helps (but does not completely) prevent identical, repeated signals
+ # from being lost when the receiving process is busy.
+ # sleep 1
end
after_fork do |server, worker|
- # Unicorn clears out signals before it forks, so rbtrace won't work
- # unless it is enabled after the fork.
- require 'rbtrace' if ENV['ENABLE_RBTRACE']
+ # Signal application hooks of worker start
+ Gitlab::Cluster::LifecycleEvents.do_worker_start
- # the following is *required* for Rails + "preload_app true",
- defined?(ActiveRecord::Base) &&
- ActiveRecord::Base.establish_connection
+ # per-process listener ports for debugging/admin/migrations
+ # addr = "127.0.0.1:#{9293 + worker.nr}"
+ # server.listen(addr, :tries => -1, :delay => 5, :tcp_nopush => true)
end
+
diff --git a/danger/documentation/Dangerfile b/danger/documentation/Dangerfile
index fe819ee250c..87c61d6e90d 100644
--- a/danger/documentation/Dangerfile
+++ b/danger/documentation/Dangerfile
@@ -15,18 +15,32 @@ docs_paths_to_review = docs_paths_requiring_review(helper.all_changed_files)
unless docs_paths_to_review.empty?
message 'This merge request adds or changes files that require a ' \
- 'review from the docs team.'
+ 'review from the Docs team.'
markdown(<<~MARKDOWN)
-## Docs Review
+## Docs review
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):
+
+| Tech writer | Stage(s) |
+| ------------ | ------------------------------------------------------------ |
+| `@marcia` | ~Create ~Release |
+| `@axil` | ~Distribution ~Gitaly ~Gitter ~Monitoring ~Packaging ~Secure |
+| `@eread` | ~Manage ~Configure ~Geo ~Verify |
+| `@mikelewis` | ~Plan |
+
+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 one of the usernames above.
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/20180910153412_add_token_digest_to_personal_access_tokens.rb b/db/migrate/20180910153412_add_token_digest_to_personal_access_tokens.rb
new file mode 100644
index 00000000000..203fcfe8eae
--- /dev/null
+++ b/db/migrate/20180910153412_add_token_digest_to_personal_access_tokens.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddTokenDigestToPersonalAccessTokens < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ change_column :personal_access_tokens, :token, :string, null: true
+
+ add_column :personal_access_tokens, :token_digest, :string
+ end
+
+ def down
+ remove_column :personal_access_tokens, :token_digest
+
+ change_column :personal_access_tokens, :token, :string, null: false
+ end
+end
diff --git a/db/migrate/20180910153413_add_index_to_token_digest_on_personal_access_tokens.rb b/db/migrate/20180910153413_add_index_to_token_digest_on_personal_access_tokens.rb
new file mode 100644
index 00000000000..4300cd13a45
--- /dev/null
+++ b/db/migrate/20180910153413_add_index_to_token_digest_on_personal_access_tokens.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexToTokenDigestOnPersonalAccessTokens < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :personal_access_tokens, :token_digest, unique: true
+ end
+
+ def down
+ remove_concurrent_index :personal_access_tokens, :token_digest if index_exists?(:personal_access_tokens, :token_digest)
+ end
+end
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/20181005110927_add_index_to_lfs_objects_file_store.rb b/db/migrate/20181005110927_add_index_to_lfs_objects_file_store.rb
new file mode 100644
index 00000000000..d09543aa4cc
--- /dev/null
+++ b/db/migrate/20181005110927_add_index_to_lfs_objects_file_store.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexToLfsObjectsFileStore < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :lfs_objects, :file_store
+ end
+
+ def down
+ remove_concurrent_index :lfs_objects, :file_store
+ 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/20181010235606_create_board_project_recent_visits.rb b/db/migrate/20181010235606_create_board_project_recent_visits.rb
new file mode 100644
index 00000000000..426f41e202a
--- /dev/null
+++ b/db/migrate/20181010235606_create_board_project_recent_visits.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class CreateBoardProjectRecentVisits < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :board_project_recent_visits, id: :bigserial do |t|
+ t.timestamps_with_timezone null: false
+
+ t.references :user, index: true, foreign_key: { on_delete: :cascade }
+ t.references :project, index: true, foreign_key: { on_delete: :cascade }
+ t.references :board, index: true, foreign_key: { on_delete: :cascade }
+ end
+
+ add_index :board_project_recent_visits, [:user_id, :project_id, :board_id], unique: true, name: 'index_board_project_recent_visits_on_user_project_and_board'
+ end
+end
diff --git a/db/migrate/20181014203236_create_cluster_groups.rb b/db/migrate/20181014203236_create_cluster_groups.rb
new file mode 100644
index 00000000000..69382d5c851
--- /dev/null
+++ b/db/migrate/20181014203236_create_cluster_groups.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class CreateClusterGroups < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :cluster_groups do |t|
+ t.references :cluster, null: false, foreign_key: { on_delete: :cascade }
+ t.references :group, null: false, index: true
+
+ t.index [:cluster_id, :group_id], unique: true
+ t.foreign_key :namespaces, column: :group_id, on_delete: :cascade
+ end
+ 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/20181016152238_create_board_group_recent_visits.rb b/db/migrate/20181016152238_create_board_group_recent_visits.rb
new file mode 100644
index 00000000000..1e55dc8658e
--- /dev/null
+++ b/db/migrate/20181016152238_create_board_group_recent_visits.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class CreateBoardGroupRecentVisits < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :board_group_recent_visits, id: :bigserial do |t|
+ t.timestamps_with_timezone null: false
+
+ t.references :user, index: true, foreign_key: { on_delete: :cascade }
+ t.references :board, index: true, foreign_key: { on_delete: :cascade }
+ t.references :group, references: :namespace, column: :group_id, index: true
+ t.foreign_key :namespaces, column: :group_id, on_delete: :cascade
+ end
+
+ add_index :board_group_recent_visits, [:user_id, :group_id, :board_id], unique: true, name: 'index_board_group_recent_visits_on_user_group_and_board'
+ end
+end
diff --git a/db/migrate/20181017001059_add_cluster_type_to_clusters.rb b/db/migrate/20181017001059_add_cluster_type_to_clusters.rb
new file mode 100644
index 00000000000..191e7eb4fb3
--- /dev/null
+++ b/db/migrate/20181017001059_add_cluster_type_to_clusters.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddClusterTypeToClusters < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ PROJECT_CLUSTER_TYPE = 3
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:clusters, :cluster_type, :smallint, default: PROJECT_CLUSTER_TYPE)
+ end
+
+ def down
+ remove_column(:clusters, :cluster_type)
+ 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/20181030154446_add_missing_indexes_for_foreign_keys.rb b/db/migrate/20181030154446_add_missing_indexes_for_foreign_keys.rb
new file mode 100644
index 00000000000..176d55565d8
--- /dev/null
+++ b/db/migrate/20181030154446_add_missing_indexes_for_foreign_keys.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+class AddMissingIndexesForForeignKeys < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index(:application_settings, :usage_stats_set_by_user_id)
+ add_concurrent_index(:ci_pipeline_schedules, :owner_id)
+ add_concurrent_index(:ci_trigger_requests, :trigger_id)
+ add_concurrent_index(:ci_triggers, :owner_id)
+ add_concurrent_index(:clusters_applications_helm, :cluster_id, unique: true)
+ add_concurrent_index(:clusters_applications_ingress, :cluster_id, unique: true)
+ add_concurrent_index(:clusters_applications_jupyter, :cluster_id, unique: true)
+ add_concurrent_index(:clusters_applications_jupyter, :oauth_application_id)
+ add_concurrent_index(:clusters_applications_knative, :cluster_id, unique: true)
+ add_concurrent_index(:clusters_applications_prometheus, :cluster_id, unique: true)
+ add_concurrent_index(:fork_network_members, :forked_from_project_id)
+ add_concurrent_index(:internal_ids, :namespace_id)
+ add_concurrent_index(:internal_ids, :project_id)
+ add_concurrent_index(:issues, :closed_by_id)
+ add_concurrent_index(:label_priorities, :label_id)
+ add_concurrent_index(:merge_request_metrics, :merged_by_id)
+ add_concurrent_index(:merge_request_metrics, :latest_closed_by_id)
+ add_concurrent_index(:oauth_openid_requests, :access_grant_id)
+ add_concurrent_index(:project_deploy_tokens, :deploy_token_id)
+ add_concurrent_index(:protected_tag_create_access_levels, :group_id)
+ add_concurrent_index(:subscriptions, :project_id)
+ add_concurrent_index(:user_statuses, :user_id)
+ add_concurrent_index(:users, :accepted_term_id)
+ end
+
+ def down
+ # MySQL requires index for FK,
+ # thus removal of indexes does fail
+ return if Gitlab::Database.mysql?
+
+ remove_concurrent_index(:application_settings, :usage_stats_set_by_user_id)
+ remove_concurrent_index(:ci_pipeline_schedules, :owner_id)
+ remove_concurrent_index(:ci_trigger_requests, :trigger_id)
+ remove_concurrent_index(:ci_triggers, :owner_id)
+ remove_concurrent_index(:clusters_applications_helm, :cluster_id, unique: true)
+ remove_concurrent_index(:clusters_applications_ingress, :cluster_id, unique: true)
+ remove_concurrent_index(:clusters_applications_jupyter, :cluster_id, unique: true)
+ remove_concurrent_index(:clusters_applications_jupyter, :oauth_application_id)
+ remove_concurrent_index(:clusters_applications_knative, :cluster_id, unique: true)
+ remove_concurrent_index(:clusters_applications_prometheus, :cluster_id, unique: true)
+ remove_concurrent_index(:fork_network_members, :forked_from_project_id)
+ remove_concurrent_index(:internal_ids, :namespace_id)
+ remove_concurrent_index(:internal_ids, :project_id)
+ remove_concurrent_index(:issues, :closed_by_id)
+ remove_concurrent_index(:label_priorities, :label_id)
+ remove_concurrent_index(:merge_request_metrics, :merged_by_id)
+ remove_concurrent_index(:merge_request_metrics, :latest_closed_by_id)
+ remove_concurrent_index(:oauth_openid_requests, :access_grant_id)
+ remove_concurrent_index(:project_deploy_tokens, :deploy_token_id)
+ remove_concurrent_index(:protected_tag_create_access_levels, :group_id)
+ remove_concurrent_index(:subscriptions, :project_id)
+ remove_concurrent_index(:user_statuses, :user_id)
+ remove_concurrent_index(:users, :accepted_term_id)
+ 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/migrate/20181112103239_drop_default_value_on_status_deployments.rb b/db/migrate/20181112103239_drop_default_value_on_status_deployments.rb
new file mode 100644
index 00000000000..a480c15e66b
--- /dev/null
+++ b/db/migrate/20181112103239_drop_default_value_on_status_deployments.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class DropDefaultValueOnStatusDeployments < ActiveRecord::Migration
+ DOWNTIME = false
+ DEPLOYMENT_STATUS_SUCCESS = 2 # Equivalent to Deployment.state_machine.states['success'].value
+
+ def up
+ change_column_default :deployments, :status, nil
+ end
+
+ def down
+ change_column_default :deployments, :status, DEPLOYMENT_STATUS_SUCCESS
+ 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/20180913142237_schedule_digest_personal_access_tokens.rb b/db/post_migrate/20180913142237_schedule_digest_personal_access_tokens.rb
new file mode 100644
index 00000000000..36be819b245
--- /dev/null
+++ b/db/post_migrate/20180913142237_schedule_digest_personal_access_tokens.rb
@@ -0,0 +1,28 @@
+class ScheduleDigestPersonalAccessTokens < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ BATCH_SIZE = 10_000
+ MIGRATION = 'DigestColumn'
+ DELAY_INTERVAL = 5.minutes.to_i
+
+ disable_ddl_transaction!
+
+ class PersonalAccessToken < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'personal_access_tokens'
+ end
+
+ def up
+ PersonalAccessToken.where('token is NOT NULL').each_batch(of: BATCH_SIZE) do |batch, index|
+ range = batch.pluck('MIN(id)', 'MAX(id)').first
+ BackgroundMigrationWorker.perform_in(index * DELAY_INTERVAL, MIGRATION, ['PersonalAccessToken', :token, :token_digest, *range])
+ end
+ end
+
+ def down
+ # raise ActiveRecord::IrreversibleMigration
+ end
+end
diff --git a/db/post_migrate/20181014121030_enqueue_redact_links.rb b/db/post_migrate/20181014121030_enqueue_redact_links.rb
new file mode 100644
index 00000000000..1ee4703c88a
--- /dev/null
+++ b/db/post_migrate/20181014121030_enqueue_redact_links.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+class EnqueueRedactLinks < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ BATCH_SIZE = 1000
+ DELAY_INTERVAL = 5.minutes.to_i
+ MIGRATION = 'RedactLinks'
+
+ disable_ddl_transaction!
+
+ class Note < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'notes'
+ self.inheritance_column = :_type_disabled
+ end
+
+ class Issue < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'issues'
+ self.inheritance_column = :_type_disabled
+ end
+
+ class MergeRequest < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'merge_requests'
+ self.inheritance_column = :_type_disabled
+ end
+
+ class Snippet < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'snippets'
+ self.inheritance_column = :_type_disabled
+ end
+
+ def up
+ disable_statement_timeout do
+ schedule_migration(Note, 'note')
+ schedule_migration(Issue, 'description')
+ schedule_migration(MergeRequest, 'description')
+ schedule_migration(Snippet, 'description')
+ end
+ end
+
+ def down
+ # nothing to do
+ end
+
+ private
+
+ def schedule_migration(model, field)
+ link_pattern = "%/sent_notifications/" + ("_" * 32) + "/unsubscribe%"
+
+ model.where("#{field} like ?", link_pattern).each_batch(of: BATCH_SIZE) do |batch, index|
+ start_id, stop_id = batch.pluck('MIN(id)', 'MAX(id)').first
+
+ BackgroundMigrationWorker.perform_in(index * DELAY_INTERVAL, MIGRATION, [model.name.demodulize, field, start_id, stop_id])
+ end
+ 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 ddfccbba678..cc47368c530 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: 20181013005024) do
+ActiveRecord::Schema.define(version: 20181112103239) 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: 20181013005024) 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,9 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
+ t.index ["usage_stats_set_by_user_id"], name: "index_application_settings_on_usage_stats_set_by_user_id", using: :btree
end
create_table "audit_events", force: :cascade do |t|
@@ -175,10 +177,9 @@ ActiveRecord::Schema.define(version: 20181013005024) 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 +187,10 @@ ActiveRecord::Schema.define(version: 20181013005024) 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,21 +199,43 @@ ActiveRecord::Schema.define(version: 20181013005024) 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
+
+ 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
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
@@ -224,10 +246,9 @@ ActiveRecord::Schema.define(version: 20181013005024) 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
@@ -238,51 +259,46 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
@@ -313,7 +329,7 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
@@ -328,45 +344,42 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
@@ -377,16 +390,15 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
@@ -394,13 +406,12 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
@@ -410,10 +421,9 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
@@ -425,11 +435,11 @@ ActiveRecord::Schema.define(version: 20181013005024) 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 ["owner_id"], name: "index_ci_pipeline_schedules_on_owner_id", 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"
@@ -437,10 +447,9 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
@@ -464,37 +473,34 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
@@ -514,14 +520,13 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
@@ -531,23 +536,22 @@ ActiveRecord::Schema.define(version: 20181013005024) 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
+ t.index ["trigger_id"], name: "index_ci_trigger_requests_on_trigger_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"
@@ -556,10 +560,10 @@ ActiveRecord::Schema.define(version: 20181013005024) do
t.integer "owner_id"
t.string "description"
t.string "ref"
+ t.index ["owner_id"], name: "index_ci_triggers_on_owner_id", using: :btree
+ 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"
@@ -569,9 +573,15 @@ ActiveRecord::Schema.define(version: 20181013005024) 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
create_table "cluster_platforms_kubernetes", force: :cascade do |t|
t.integer "cluster_id", null: false
@@ -586,20 +596,18 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
@@ -615,10 +623,9 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
@@ -628,11 +635,11 @@ ActiveRecord::Schema.define(version: 20181013005024) do
t.boolean "enabled", default: true
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
@@ -643,6 +650,7 @@ ActiveRecord::Schema.define(version: 20181013005024) do
t.text "encrypted_ca_key"
t.text "encrypted_ca_key_iv"
t.text "ca_cert"
+ t.index ["cluster_id"], name: "index_clusters_applications_helm_on_cluster_id", unique: true, using: :btree
end
create_table "clusters_applications_ingress", force: :cascade do |t|
@@ -655,6 +663,7 @@ ActiveRecord::Schema.define(version: 20181013005024) do
t.string "cluster_ip"
t.text "status_reason"
t.string "external_ip"
+ t.index ["cluster_id"], name: "index_clusters_applications_ingress_on_cluster_id", unique: true, using: :btree
end
create_table "clusters_applications_jupyter", force: :cascade do |t|
@@ -666,6 +675,19 @@ ActiveRecord::Schema.define(version: 20181013005024) do
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.text "status_reason"
+ t.index ["cluster_id"], name: "index_clusters_applications_jupyter_on_cluster_id", unique: true, using: :btree
+ t.index ["oauth_application_id"], name: "index_clusters_applications_jupyter_on_oauth_application_id", using: :btree
+ end
+
+ create_table "clusters_applications_knative", force: :cascade do |t|
+ 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"
+ t.index ["cluster_id"], name: "index_clusters_applications_knative_on_cluster_id", unique: true, using: :btree
end
create_table "clusters_applications_prometheus", force: :cascade do |t|
@@ -675,6 +697,7 @@ ActiveRecord::Schema.define(version: 20181013005024) do
t.text "status_reason"
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
+ t.index ["cluster_id"], name: "index_clusters_applications_prometheus_on_cluster_id", unique: true, using: :btree
end
create_table "clusters_applications_runners", force: :cascade do |t|
@@ -686,11 +709,10 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
@@ -701,23 +723,21 @@ ActiveRecord::Schema.define(version: 20181013005024) 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
@@ -759,10 +779,9 @@ ActiveRecord::Schema.define(version: 20181013005024) 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
@@ -771,11 +790,10 @@ ActiveRecord::Schema.define(version: 20181013005024) 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
@@ -789,14 +807,19 @@ ActiveRecord::Schema.define(version: 20181013005024) do
t.datetime "created_at"
t.datetime "updated_at"
t.string "on_stop"
+ t.integer "status", limit: 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
@@ -805,12 +828,11 @@ ActiveRecord::Schema.define(version: 20181013005024) 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
@@ -820,11 +842,10 @@ ActiveRecord::Schema.define(version: 20181013005024) 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
@@ -833,95 +854,60 @@ ActiveRecord::Schema.define(version: 20181013005024) 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 ["forked_from_project_id"], name: "index_fork_network_members_on_forked_from_project_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
@@ -929,12 +915,11 @@ ActiveRecord::Schema.define(version: 20181013005024) 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
@@ -946,63 +931,59 @@ ActiveRecord::Schema.define(version: 20181013005024) 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 ["namespace_id"], name: "index_internal_ids_on_namespace_id", using: :btree
+ t.index ["project_id"], name: "index_internal_ids_on_project_id", using: :btree
+ 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"
@@ -1010,10 +991,9 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
@@ -1039,23 +1019,23 @@ ActiveRecord::Schema.define(version: 20181013005024) 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 ["closed_by_id"], name: "index_issues_on_closed_by_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"
@@ -1066,33 +1046,31 @@ ActiveRecord::Schema.define(version: 20181013005024) 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 ["label_id"], name: "index_label_priorities_on_label_id", using: :btree
+ 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"
@@ -1105,44 +1083,41 @@ ActiveRecord::Schema.define(version: 20181013005024) 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", ["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"
@@ -1150,12 +1125,11 @@ ActiveRecord::Schema.define(version: 20181013005024) 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
@@ -1171,14 +1145,13 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
@@ -1190,11 +1163,10 @@ ActiveRecord::Schema.define(version: 20181013005024) 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
@@ -1208,10 +1180,9 @@ ActiveRecord::Schema.define(version: 20181013005024) 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
@@ -1222,10 +1193,9 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
@@ -1238,12 +1208,13 @@ ActiveRecord::Schema.define(version: 20181013005024) 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 ["latest_closed_by_id"], name: "index_merge_request_metrics_on_latest_closed_by_id", using: :btree
+ t.index ["merge_request_id"], name: "index_merge_request_metrics", using: :btree
+ t.index ["merged_by_id"], name: "index_merge_request_metrics_on_merged_by_id", 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
@@ -1280,37 +1251,36 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
@@ -1325,15 +1295,14 @@ ActiveRecord::Schema.define(version: 20181013005024) 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
@@ -1353,19 +1322,18 @@ ActiveRecord::Schema.define(version: 20181013005024) 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
@@ -1376,10 +1344,9 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
@@ -1404,19 +1371,18 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
@@ -1438,12 +1404,11 @@ ActiveRecord::Schema.define(version: 20181013005024) 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
@@ -1453,10 +1418,9 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
@@ -1466,12 +1430,11 @@ ActiveRecord::Schema.define(version: 20181013005024) 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
@@ -1483,14 +1446,14 @@ ActiveRecord::Schema.define(version: 20181013005024) 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
+ t.index ["access_grant_id"], name: "index_oauth_openid_requests_on_access_grant_id", using: :btree
end
create_table "pages_domains", force: :cascade do |t|
@@ -1503,17 +1466,16 @@ ActiveRecord::Schema.define(version: 20181013005024) 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", null: false
+ t.string "token"
t.string "name", null: false
t.boolean "revoked", default: false
t.date "expires_at"
@@ -1521,28 +1483,27 @@ ActiveRecord::Schema.define(version: 20181013005024) do
t.datetime "updated_at", null: false
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", ["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
@@ -1550,51 +1511,47 @@ ActiveRecord::Schema.define(version: 20181013005024) 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
@@ -1602,45 +1559,41 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
@@ -1693,28 +1646,29 @@ ActiveRecord::Schema.define(version: 20181013005024) 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
@@ -1727,40 +1681,36 @@ ActiveRecord::Schema.define(version: 20181013005024) 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
@@ -1768,23 +1718,22 @@ ActiveRecord::Schema.define(version: 20181013005024) do
t.integer "group_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.index ["group_id"], name: "index_protected_tag_create_access_levels_on_group_id", using: :btree
+ 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
@@ -1792,21 +1741,19 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
@@ -1815,11 +1762,10 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
@@ -1836,19 +1782,24 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
@@ -1859,13 +1810,12 @@ ActiveRecord::Schema.define(version: 20181013005024) 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
@@ -1873,12 +1823,11 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
@@ -1890,10 +1839,9 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
@@ -1916,10 +1864,14 @@ ActiveRecord::Schema.define(version: 20181013005024) 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
@@ -1940,15 +1892,14 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
@@ -1971,20 +1922,19 @@ ActiveRecord::Schema.define(version: 20181013005024) do
t.datetime "created_at"
t.datetime "updated_at"
t.integer "project_id"
+ t.index ["project_id"], name: "index_subscriptions_on_project_id", using: :btree
+ 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"
@@ -1993,32 +1943,29 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
@@ -2027,12 +1974,11 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
@@ -2046,24 +1992,22 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
@@ -2073,13 +2017,12 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
@@ -2089,12 +2032,12 @@ ActiveRecord::Schema.define(version: 20181013005024) 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
@@ -2103,52 +2046,48 @@ ActiveRecord::Schema.define(version: 20181013005024) 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
t.string "message", limit: 100
t.string "message_html"
+ t.index ["user_id"], name: "index_user_statuses_on_user_id", using: :btree
end
create_table "user_synced_attributes_metadata", force: :cascade do |t|
@@ -2157,10 +2096,9 @@ ActiveRecord::Schema.define(version: 20181013005024) 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
@@ -2230,33 +2168,32 @@ ActiveRecord::Schema.define(version: 20181013005024) do
t.boolean "private_profile"
t.boolean "include_private_contributions"
t.string "commit_email"
+ t.index ["accepted_term_id"], name: "index_users_on_accepted_term_id", using: :btree
+ 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"
@@ -2270,11 +2207,10 @@ ActiveRecord::Schema.define(version: 20181013005024) 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"
@@ -2298,14 +2234,19 @@ ActiveRecord::Schema.define(version: 20181013005024) 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
+ add_foreign_key "board_group_recent_visits", "boards", on_delete: :cascade
+ add_foreign_key "board_group_recent_visits", "namespaces", column: "group_id", on_delete: :cascade
+ add_foreign_key "board_group_recent_visits", "users", on_delete: :cascade
+ add_foreign_key "board_project_recent_visits", "boards", on_delete: :cascade
+ add_foreign_key "board_project_recent_visits", "projects", on_delete: :cascade
+ add_foreign_key "board_project_recent_visits", "users", on_delete: :cascade
add_foreign_key "boards", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "boards", "projects", name: "fk_f15266b5f9", on_delete: :cascade
add_foreign_key "chat_teams", "namespaces", on_delete: :cascade
@@ -2340,6 +2281,8 @@ ActiveRecord::Schema.define(version: 20181013005024) do
add_foreign_key "ci_triggers", "projects", name: "fk_e3e63f966e", on_delete: :cascade
add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", on_delete: :cascade
add_foreign_key "ci_variables", "projects", name: "fk_ada5eb64b3", on_delete: :cascade
+ add_foreign_key "cluster_groups", "clusters", on_delete: :cascade
+ add_foreign_key "cluster_groups", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "cluster_platforms_kubernetes", "clusters", on_delete: :cascade
add_foreign_key "cluster_projects", "clusters", on_delete: :cascade
add_foreign_key "cluster_projects", "projects", on_delete: :cascade
@@ -2349,6 +2292,7 @@ ActiveRecord::Schema.define(version: 20181013005024) 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
@@ -2366,9 +2310,6 @@ ActiveRecord::Schema.define(version: 20181013005024) 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
@@ -2435,6 +2376,7 @@ ActiveRecord::Schema.define(version: 20181013005024) 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
@@ -2446,6 +2388,7 @@ ActiveRecord::Schema.define(version: 20181013005024) 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/README.md b/doc/README.md
index 03371226041..20fcd2e1724 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -165,6 +165,7 @@ configuration. Then customize everything from buildpacks to CI/CD.
- [Deployment of Helm, Ingress, and Prometheus on Kubernetes](user/project/clusters/index.md#installing-applications)
- [Protected variables](ci/variables/README.md#protected-variables)
- [Easy creation of Kubernetes clusters on GKE](user/project/clusters/index.md#adding-and-creating-a-new-gke-cluster-via-gitlab)
+- [Executable Runbooks](user/project/clusters/runbooks/index.md)
### Monitor
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..341a00009e5 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,
@@ -21,7 +19,15 @@ and the [protocol documentation](https://github.com/git/git/blob/master/Document
From the client side, `git` `v2.18.0` or newer must be installed.
From the server side, if we want to configure SSH we need to set the `sshd`
-server to accept the `GIT_PROTOCOL` environment,
+server to accept the `GIT_PROTOCOL` environment.
+
+In installations using [GitLab Helm Charts](../install/kubernetes/gitlab_chart.md)
+and [All-in-one docker image](https://docs.gitlab.com/omnibus/docker/), the SSH
+service is already configured to accept the `GIT_PROTOCOL` environment and users
+need not do anything more.
+
+For Omnibus GitLab and installations from source, you have to manually update
+the SSH configuration of your server:
```
# /etc/ssh/sshd_config
diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md
index 96803746637..74b0e2c8184 100644
--- a/doc/administration/high_availability/nfs.md
+++ b/doc/administration/high_availability/nfs.md
@@ -59,9 +59,9 @@ 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`.
-## AWS Elastic File System
+## Avoid using AWS's Elastic File System (EFS)
GitLab strongly recommends against using AWS Elastic File System (EFS).
Our support team will not be able to assist on performance issues related to
@@ -78,6 +78,23 @@ stored on a local volume.
For more details on another person's experience with EFS, see
[Amazon's Elastic File System: Burst Credits](https://rawkode.com/2017/04/16/amazons-elastic-file-system-burst-credits/)
+## Avoid using PostgreSQL with NFS
+
+GitLab strongly recommends against running your PostgreSQL database
+across NFS. The GitLab support team will not be able to assist on performance issues related to
+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
+>to the NFS server can cause data corruption problems.
+
+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
Below is an example of an NFS mount point defined in `/etc/fstab` we use on
diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md
index b5d1ff698c6..a9ba40c870c 100644
--- a/doc/administration/high_availability/redis.md
+++ b/doc/administration/high_availability/redis.md
@@ -15,7 +15,7 @@ Omnibus GitLab packages.
> **Notes:**
> - Redis requires authentication for High Availability. See
-> [Redis Security](http://redis.io/topics/security) documentation for more
+> [Redis Security](https://redis.io/topics/security) documentation for more
> information. We recommend using a combination of a Redis password and tight
> firewall rules to secure your Redis service.
> - You are highly encouraged to read the [Redis Sentinel][sentinel] documentation
@@ -82,7 +82,7 @@ When a **Master** fails to respond, it's the application's responsibility
for a new **Master**).
To get a better understanding on how to correctly set up Sentinel, please read
-the [Redis Sentinel documentation](http://redis.io/topics/sentinel) first, as
+the [Redis Sentinel documentation](https://redis.io/topics/sentinel) first, as
failing to configure it correctly can lead to data loss or can bring your
whole cluster down, invalidating the failover effort.
@@ -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
@@ -684,7 +684,7 @@ cache, queues, and shared_state. To make this work with Sentinel:
```
1. Note that for each persistence class, GitLab will default to using the
configuration specified in `gitlab_rails['redis_sentinels']` unless
- overriden by the settings above.
+ overridden by the settings above.
1. Be sure to include BOTH configuration options for each persistent classes. For example,
if you choose to configure a cache instance, you must specify both `gitlab_rails['redis_cache_instance']`
and `gitlab_rails['redis_cache_sentinels']` for GitLab to generate the proper configuration files.
@@ -885,8 +885,8 @@ Read more on High Availability:
[reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
[gh-531]: https://github.com/redis/redis-rb/issues/531
[gh-534]: https://github.com/redis/redis-rb/issues/534
-[redis]: http://redis.io/
-[sentinel]: http://redis.io/topics/sentinel
+[redis]: https://redis.io/
+[sentinel]: https://redis.io/topics/sentinel
[omnifile]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/libraries/gitlab_rails.rb
[source]: ../../install/installation.md
[ce]: https://about.gitlab.com/downloads
diff --git a/doc/administration/high_availability/redis_source.md b/doc/administration/high_availability/redis_source.md
index 5823c575251..2101d36d2b6 100644
--- a/doc/administration/high_availability/redis_source.md
+++ b/doc/administration/high_availability/redis_source.md
@@ -107,7 +107,7 @@ starting with `sentinel` prefix.
Assuming that the Redis Sentinel is installed on the same instance as Redis
master with IP `10.0.0.1` (some settings might overlap with the master):
-1. [Install Redis Sentinel](http://redis.io/topics/sentinel)
+1. [Install Redis Sentinel](https://redis.io/topics/sentinel)
1. Edit `/etc/redis/sentinel.conf`:
```conf
@@ -363,7 +363,7 @@ production:
port: 26379 # point to sentinel, not to redis port
```
-When in doubt, please read [Redis Sentinel documentation](http://redis.io/topics/sentinel).
+When in doubt, please read [Redis Sentinel documentation](https://redis.io/topics/sentinel).
[gh-531]: https://github.com/redis/redis-rb/issues/531
[downloads]: https://about.gitlab.com/downloads
diff --git a/doc/administration/logs.md b/doc/administration/logs.md
index 0e41a38b7e2..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`.
@@ -84,7 +84,7 @@ Introduced in GitLab 10.0, this file lives in
It helps you see requests made directly to the API. For example:
```json
-{"time":"2017-10-10T12:30:11.579Z","severity":"INFO","duration":16.84,"db":1.57,"view":15.27,"status":200,"method":"POST","path":"/api/v4/internal/allowed","params":{"action":"git-upload-pack","changes":"_any","gl_repository":null,"project":"root/foobar.git","protocol":"ssh","env":"{}","key_id":"[FILTERED]","secret_token":"[FILTERED]"},"host":"127.0.0.1","ip":"127.0.0.1","ua":"Ruby"}
+{"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}
```
This entry above shows an access to an internal endpoint to check whether an
@@ -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..2d9fdedcbeb 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,10 +24,10 @@ 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.
+Prometheus and its exporters are on by default, starting with GitLab 9.0.
Prometheus will run as the `gitlab-prometheus` user and listen on
`http://localhost:9090`. By default Prometheus is only accessible from the GitLab server itself.
Each exporter will be automatically set up as a
@@ -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/administration/uploads.md b/doc/administration/uploads.md
index aec9a359ada..f85a1f791f9 100644
--- a/doc/administration/uploads.md
+++ b/doc/administration/uploads.md
@@ -48,11 +48,12 @@ _The uploads are stored by default in
1. Save the file and [restart GitLab][] for the changes to take effect.
-### Using object storage
+### Using object storage **[CORE ONLY]**
> **Notes:**
>
-> - [Introduced][ee-3867] in [GitLab Enterprise Edition Premium][eep] 10.5.
+> - [Introduced][ee-3867] in [GitLab Premium][eep] 10.5.
+> - [Introduced][ce17358] in [GitLab Core][ce] 10.7.
> - Since version 11.1, we support direct_upload to S3.
If you don't want to use the local disk where GitLab is installed to store the
@@ -197,4 +198,6 @@ _The uploads are stored by default in
[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab"
[restart gitlab]: restart_gitlab.md#installations-from-source "How to restart GitLab"
[eep]: https://about.gitlab.com/gitlab-ee/ "GitLab Enterprise Edition Premium"
+[ce]: https://about.gitlab.com/gitlab-ce/ "GitLab Community Edition"
[ee-3867]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3867
+[ce-17358]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17358
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/avatar.md b/doc/api/avatar.md
index 7faed893066..aa6f7c185ae 100644
--- a/doc/api/avatar.md
+++ b/doc/api/avatar.md
@@ -4,7 +4,7 @@
## Get a single avatar URL
-Get a single avatar URL for a given email addres. If user with matching public
+Get a single avatar URL for a given email address. If user with matching public
email address is not found, results from external avatar services are returned.
This endpoint can be accessed without authentication. In case public visibility
is restricted, response will be `403 Forbidden` when unauthenticated.
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 9b7ca4b6e70..7d9b52ec24f 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.5.
+
+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 cc1d6834a20..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` _([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)_ |
+| `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`. `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` _([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)_ |
+| `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`. `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` _([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)_ |
+| `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`. `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 862ee398a84..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 |
@@ -42,12 +42,12 @@ Parameters:
| `updated_before` | datetime | no | Return merge requests updated on or before the given time |
| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead. |
| `author_id` | integer | no | Returns merge requests created by the given user `id`. Combine with `scope=all` or `scope=assigned_to_me` |
-| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id` |
-| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
+| `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`. `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 |
@@ -166,10 +177,10 @@ Parameters:
| `updated_before` | datetime | no | Return merge requests updated on or before the given time |
| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13060] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ |
| `author_id` | integer | no | Returns merge requests created by the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ |
-| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id` _([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 |
+| `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`. `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 |
@@ -279,10 +301,10 @@ Parameters:
| `updated_before` | datetime | no | Return merge requests updated on or before the given time |
| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> |
| `author_id` | integer | no | Returns merge requests created by the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ |
-| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id` _([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 |
+| `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`. `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/pipelines.md b/doc/api/pipelines.md
index 574be52801c..7b4c9a8fbb3 100644
--- a/doc/api/pipelines.md
+++ b/doc/api/pipelines.md
@@ -235,5 +235,22 @@ Response:
}
```
+## Delete a pipeline
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22988) in GitLab 11.6.
+
+```
+DELETE /projects/:id/pipelines/:pipeline_id
+```
+
+| Attribute | Type | Required | Description |
+|------------|---------|----------|---------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `pipeline_id` | integer | yes | The ID of a pipeline |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --request "DELETE" "https://gitlab.example.com/api/v4/projects/1/pipelines/46"
+```
+
[ce-5837]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5837
[ce-7209]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7209
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 947e7db9c52..961241f31e1 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -451,6 +451,7 @@ GET /projects/:id
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `statistics` | boolean | no | Include project statistics |
+| `license` | boolean | no | Include project license data |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
```json
@@ -508,6 +509,14 @@ GET /projects/:id
},
"archived": false,
"avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png",
+ "license_url": "http://example.com/diaspora/diaspora-client/blob/master/LICENSE",
+ "license": {
+ "key": "lgpl-3.0",
+ "name": "GNU Lesser General Public License v3.0",
+ "nickname": "GNU LGPLv3",
+ "html_url": "http://choosealicense.com/licenses/lgpl-3.0/",
+ "source_url": "http://www.gnu.org/licenses/lgpl-3.0.txt"
+ },
"shared_runners_enabled": true,
"forks_count": 0,
"star_count": 0,
@@ -572,6 +581,14 @@ If the project is a fork, and you provide a valid token to authenticate, the
"http_url_to_repo":"https://gitlab.com/gitlab-org/gitlab-ce.git",
"web_url":"https://gitlab.com/gitlab-org/gitlab-ce",
"avatar_url":"https://assets.gitlab-static.net/uploads/-/system/project/avatar/13083/logo-extra-whitespace.png",
+ "license_url": "https://gitlab.com/gitlab-org/gitlab-ce/blob/master/LICENSE",
+ "license": {
+ "key": "mit",
+ "name": "MIT License",
+ "nickname": null,
+ "html_url": "http://choosealicense.com/licenses/mit/",
+ "source_url": "https://opensource.org/licenses/MIT",
+ },
"star_count":3812,
"forks_count":3561,
"last_activity_at":"2018-01-02T11:40:26.570Z",
@@ -905,6 +922,14 @@ Example response:
"import_status": "none",
"archived": true,
"avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png",
+ "license_url": "http://example.com/diaspora/diaspora-client/blob/master/LICENSE",
+ "license": {
+ "key": "lgpl-3.0",
+ "name": "GNU Lesser General Public License v3.0",
+ "nickname": "GNU LGPLv3",
+ "html_url": "http://choosealicense.com/licenses/lgpl-3.0/",
+ "source_url": "http://www.gnu.org/licenses/lgpl-3.0.txt"
+ },
"shared_runners_enabled": true,
"forks_count": 0,
"star_count": 1,
@@ -983,6 +1008,14 @@ Example response:
"import_status": "none",
"archived": true,
"avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png",
+ "license_url": "http://example.com/diaspora/diaspora-client/blob/master/LICENSE",
+ "license": {
+ "key": "lgpl-3.0",
+ "name": "GNU Lesser General Public License v3.0",
+ "nickname": "GNU LGPLv3",
+ "html_url": "http://choosealicense.com/licenses/lgpl-3.0/",
+ "source_url": "http://www.gnu.org/licenses/lgpl-3.0.txt"
+ },
"shared_runners_enabled": true,
"forks_count": 0,
"star_count": 0,
@@ -1101,6 +1134,14 @@ Example response:
},
"archived": true,
"avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png",
+ "license_url": "http://example.com/diaspora/diaspora-client/blob/master/LICENSE",
+ "license": {
+ "key": "lgpl-3.0",
+ "name": "GNU Lesser General Public License v3.0",
+ "nickname": "GNU LGPLv3",
+ "html_url": "http://choosealicense.com/licenses/lgpl-3.0/",
+ "source_url": "http://www.gnu.org/licenses/lgpl-3.0.txt"
+ },
"shared_runners_enabled": true,
"forks_count": 0,
"star_count": 0,
@@ -1197,6 +1238,14 @@ Example response:
},
"archived": false,
"avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png",
+ "license_url": "http://example.com/diaspora/diaspora-client/blob/master/LICENSE",
+ "license": {
+ "key": "lgpl-3.0",
+ "name": "GNU Lesser General Public License v3.0",
+ "nickname": "GNU LGPLv3",
+ "html_url": "http://choosealicense.com/licenses/lgpl-3.0/",
+ "source_url": "http://www.gnu.org/licenses/lgpl-3.0.txt"
+ },
"shared_runners_enabled": true,
"forks_count": 0,
"star_count": 0,
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/api/tags.md b/doc/api/tags.md
index f2a3f9f28d2..826900ca518 100644
--- a/doc/api/tags.md
+++ b/doc/api/tags.md
@@ -134,7 +134,7 @@ Parameters:
"description": "Amazing release. Wow"
},
"name": "v1.0.0",
- "target: "2695effb5807a22ff3d138d593fd856244e155e7",
+ "target": "2695effb5807a22ff3d138d593fd856244e155e7",
"message": null
}
```
diff --git a/doc/api/users.md b/doc/api/users.md
index ee24aa09156..e3633c46041 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -1072,7 +1072,6 @@ Example response:
[
{
"active" : true,
- "token" : "EsMo-vhKfXGwX9RKrwiy",
"scopes" : [
"api"
],
@@ -1089,7 +1088,6 @@ Example response:
"read_user"
],
"revoked" : true,
- "token" : "ZcZRpLeEuQRprkRjYydY",
"name" : "mytoken2",
"created_at" : "2017-03-17T17:19:28.697Z",
"id" : 3,
@@ -1125,7 +1123,6 @@ Example response:
```json
{
"active" : true,
- "token" : "EsMo-vhKfXGwX9RKrwiy",
"scopes" : [
"api"
],
@@ -1142,6 +1139,8 @@ Example response:
> Requires admin permissions.
+> Token values are returned once. Make sure you save it - you won't be able to access it again.
+
It creates a new impersonation token. Note that only administrators can do this.
You are only able to create impersonation tokens to impersonate the user and perform
both API calls and Git reads and writes. The user will not see these tokens in their profile
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 0f79f7d1b17..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,31 +43,77 @@ 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).
+For [GitLab Ultimate][ee] users, this information will
+be automatically extracted and shown right in the merge request widget.
+[Learn more on Container Scanning in merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/container_scanning.html).
+
+CAUTION: **Caution:**
+Starting with GitLab 11.5, Container Scanning feature is licensed under the name `container_scanning`.
+While the old name `sast_container` is still maintained, it has been deprecated with GitLab 11.5 and
+may be removed in next major release, GitLab 12.0. You are advised to update your current `.gitlab-ci.yml`
+configuration to reflect that change if you are using the `$GITLAB_FEATURES` environment variable.
+
+## Previous job definitions
CAUTION: **Caution:**
-Before GitLab 11.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.
+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/container_registry_checkbox.png b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/container_registry_checkbox.png
deleted file mode 100644
index a56c07a0da7..00000000000
--- a/doc/ci/examples/laravel_with_gitlab_and_envoy/img/container_registry_checkbox.png
+++ /dev/null
Binary files differ
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..b090ea014dc 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
@@ -444,9 +444,7 @@ On your GitLab project repository navigate to the **Registry** tab.
![container registry page empty image](img/container_registry_page_empty_image.png)
-You may need to [enable Container Registry](../../../user/project/container_registry.md#enable-the-container-registry-for-your-project) to your project to see this tab. You'll find it under your project's **Settings > General > Sharing and permissions**.
-
-![container registry checkbox](img/container_registry_checkbox.png)
+You may need to [enable Container Registry](../../../user/project/container_registry.md#enable-the-container-registry-for-your-project) to your project to see this tab. You'll find it under your project's **Settings > General > Permissions**.
To start using Container Registry on our machine, we first need to login to the GitLab registry using our GitLab username and password:
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/examples/test_phoenix_app_with_gitlab_ci_cd/index.md b/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md
index b2c73caae2e..c0346d78141 100644
--- a/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md
+++ b/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md
@@ -398,10 +398,10 @@ other reasons][ci-reasons] to keep using GitLab CI/CD. The benefits to our teams
- [Using Docker images documentation][using-docker]
- [Example project: Hello GitLab CI/CD on GitLab][hello-gitlab]
-[phoenix-site]: http://phoenixframework.org/ "Phoenix Framework"
+[phoenix-site]: https://phoenixframework.org/ "Phoenix Framework"
[phoenix-learning-guide]: https://hexdocs.pm/phoenix/learning.html "Phoenix Learning Guide"
-[phoenix-install]: http://www.phoenixframework.org/docs/installation "Phoenix Installation"
-[phoenix-mysql]: http://www.phoenixframework.org/docs/using-mysql "Phoenix with MySQL"
+[phoenix-install]: https://hexdocs.pm/phoenix/installation.html "Phoenix Installation"
+[phoenix-mysql]: https://hexdocs.pm/phoenix/ecto.html#using-mysql "Phoenix with MySQL"
[elixir-site]: http://elixir-lang.org/ "Elixir"
[elixir-mix]: http://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html "Introduction to mix"
[elixir-docs]: http://elixir-lang.org/getting-started/introduction.html "Elixir Documentation"
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index 371703a12c8..c628895ee1a 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -294,7 +294,7 @@ runners will not use regular runners, they must be tagged accordingly.
[jobs]: #jobs
[jobs-yaml]: yaml/README.md#jobs
-[manual]: yaml/README.md#manual
+[manual]: yaml/README.md#whenmanual
[env-manual]: environments.md#manually-deploying-to-environments
[stages]: yaml/README.md#stages
[runners]: runners/README.html
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/variables/where_variables_can_be_used.md b/doc/ci/variables/where_variables_can_be_used.md
index 4e8ce10c9cb..1d98e8426fe 100644
--- a/doc/ci/variables/where_variables_can_be_used.md
+++ b/doc/ci/variables/where_variables_can_be_used.md
@@ -17,7 +17,7 @@ There are two places defined variables can be used. On the:
| Definition | Can be expanded? | Expansion place | Description |
|--------------------------------------|-------------------|-----------------|--------------|
-| `environment:url` | yes | GitLab | The variable expansion is made by GitLab's [internal variable expansion mechanism](#gitlab-internal-variable-expansion-mechanism).<ul><li>Supported: all variables defined for a job (project/group variables, variables from `.gitlab-ci.yml`, variables from triggers, variables from pipeline schedules)</li><li>Not suported: variables defined in Runner's `config.toml` and variables created in job's `script`</li></ul> |
+| `environment:url` | yes | GitLab | The variable expansion is made by GitLab's [internal variable expansion mechanism](#gitlab-internal-variable-expansion-mechanism).<ul><li>Supported: all variables defined for a job (project/group variables, variables from `.gitlab-ci.yml`, variables from triggers, variables from pipeline schedules)</li><li>Not supported: variables defined in Runner's `config.toml` and variables created in job's `script`</li></ul> |
| `environment:name` | yes | GitLab | Similar to `environment:url`, but the variables expansion doesn't support: <ul><li>variables that are based on the environment's name (`CI_ENVIRONMENT_NAME`, `CI_ENVIRONMENT_SLUG`)</li><li>any other variables related to environment (currently only `CI_ENVIRONMENT_URL`)</li><li>[persisted variables](#persisted-variables)</li></ul> |
| `variables` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
| `image` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 4b2a6ccc7e4..44eec43ef54 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.
@@ -474,6 +475,8 @@ docker build:
changes:
- Dockerfile
- docker/scripts/*
+ - dockerfiles/**/*
+ - more_scripts/*.{rb,py,sh}
```
In the scenario above, if you are pushing multiple commits to GitLab to an
@@ -482,6 +485,8 @@ one of the commits contains changes to either:
- The `Dockerfile` file.
- Any of the files inside `docker/scripts/` directory.
+- Any of the files and subfolders inside `dockerfiles` directory.
+- Any of the files with `rb`, `py`, `sh` extensions inside `more_scripts` directory.
CAUTION: **Warning:**
There are some caveats when using this feature with new branches and tags. See
@@ -809,7 +814,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.
@@ -1334,6 +1339,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.
@@ -1430,18 +1510,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:
@@ -1449,12 +1531,84 @@ 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.
-> Available for Starter, Premium and Ultimate [versions][gitlab-versions] since 10.6.
+> Introduced in [GitLab Premium](https://about.gitlab.com/pricing/) 10.5.
+> Available for Starter, Premium and Ultimate since 10.6.
> Behaviour expanded in GitLab 10.8 to allow more flexible overriding.
-> Available for Libre since [11.4](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21603)
+> [Moved](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21603)
+to GitLab Core in 11.4
Using the `include` keyword, you can allow the inclusion of external YAML files.
@@ -1528,6 +1682,11 @@ include:
NOTE: **Note:**
The remote file must be publicly accessible through a simple GET request, as we don't support authentication schemas in the remote URL.
+ NOTE: **Note:**
+ In order to include files from another repository inside your local network,
+ you may need to enable the **Allow requests to the local network from hooks and services** checkbox
+ located in the **Settings > Network > Outbound requests** section within the **Admin area**.
+
---
@@ -1616,7 +1775,7 @@ stages:
production:
script:
- - install_depedencies
+ - install_dependencies
- deploy
- notify_owner
```
@@ -2032,4 +2191,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/architecture.md b/doc/development/architecture.md
index 66d8a4f2f6e..01d99c46f89 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -10,39 +10,182 @@ For information, see the [GitLab Release Process](https://gitlab.com/gitlab-org/
Both EE and CE require some add-on components called gitlab-shell and Gitaly. These components are available from the [gitlab-shell](https://gitlab.com/gitlab-org/gitlab-shell/tree/master) and [gitaly](https://gitlab.com/gitlab-org/gitaly/tree/master) repositories respectively. New versions are usually tags but staying on the master branch will give you the latest stable version. New releases are generally around the same time as GitLab CE releases with exception for informal security updates deemed critical.
-## Physical office analogy
+## GitLab Omnibus Component by Component
-You can imagine GitLab as a physical office.
+This document is designed to be consumed by systems adminstrators and GitLab Support Engineers who want to understand more about the internals of GitLab and how they work together.
-**The repositories** are the goods GitLab handles.
-They can be stored in a warehouse.
-This can be either a hard disk, or something more complex, such as a NFS filesystem;
+When deployed, GitLab should be considered the amalgamation of the below processes. When troubleshooting or debugging, be as specific as possible as to which component you are referencing. That should increase clarity and reduce confusion.
-**Nginx** acts like the front-desk.
-Users come to Nginx and request actions to be done by workers in the office;
+### GitLab Process Descriptions
-**The database** is a series of metal file cabinets with information on:
- - The goods in the warehouse (metadata, issues, merge requests etc);
- - The users coming to the front desk (permissions)
+As of this writing, a fresh GitLab 11.3.0 install will show the following processes with `gitlab-ctl status`:
-**Redis** is a communication board with “cubby holes” that can contain tasks for office workers;
+```
+run: alertmanager: (pid 30829) 14207s; run: log: (pid 13906) 2432044s
+run: gitaly: (pid 30771) 14210s; run: log: (pid 13843) 2432046s
+run: gitlab-monitor: (pid 30788) 14209s; run: log: (pid 13868) 2432045s
+run: gitlab-workhorse: (pid 30758) 14210s; run: log: (pid 13855) 2432046s
+run: logrotate: (pid 30246) 3407s; run: log: (pid 13825) 2432047s
+run: nginx: (pid 30849) 14207s; run: log: (pid 13856) 2432046s
+run: node-exporter: (pid 30929) 14206s; run: log: (pid 13877) 2432045s
+run: postgres-exporter: (pid 30935) 14206s; run: log: (pid 13931) 2432044s
+run: postgresql: (pid 13133) 2432214s; run: log: (pid 13848) 2432046s
+run: prometheus: (pid 30807) 14209s; run: log: (pid 13884) 2432045s
+run: redis: (pid 30560) 14274s; run: log: (pid 13807) 2432047s
+run: redis-exporter: (pid 30946) 14205s; run: log: (pid 13869) 2432045s
+run: sidekiq: (pid 30953) 14205s; run: log: (pid 13810) 2432047s
+run: unicorn: (pid 30960) 14204s; run: log: (pid 13809) 2432047s
+```
+
+### Layers
+
+GitLab can be considered to have two layers from a process perspective:
+
+- **Monitoring**: Anything from this layer is not required to deliver GitLab the application, but will allow administrators more insight into their infrastructure and what the service as a whole is doing.
+- **Core**: Any process that is vital for the delivery of GitLab as as platform. If any of these processes halt there will be a GitLab outage. For the Core layer, you can further divide into:
+ - **Processors**: These processes are responsible for actually performing operations and presenting the service.
+ - **Data**: These services store/expose structured data for the GitLab service.
+
+### alertmanager
+
+- Omnibus configuration options
+- Layer: Monitoring
+
+[Alert manager](https://prometheus.io/docs/alerting/alertmanager/) is a tool provided by prometheus that _"handles alerts sent by client applications such as the Prometheus server. It takes care of deduplicating, grouping, and routing them to the correct receiver integration such as email, PagerDuty, or OpsGenie. It also takes care of silencing and inhibition of alerts."_ You can read more in [issue gitlab-ce#45740](https://gitlab.com/gitlab-org/gitlab-ce/issues/45740) about what we will be alerting on.
+
+### gitaly
+
+- [Omnibus confiugration options](https://gitlab.com/gitlab-org/gitaly/tree/master/doc/configuration)
+- Layer: Core Service (Data)
+
+Gitaly is a service designed by GitLab to remove our need for NFS for Git storage in distributed deployments of GitLab. (Think GitLab.com or High Availablity Deployments) As of 11.3.0, This service handles all Git level access in GitLab. You can read more about the project [in the project's readme](https://gitlab.com/gitlab-org/gitaly).
+
+### gitlab-monitor
+
+- Omnibus configuration options
+- Layer: Monitoring
+
+GitLab Monitor is a process disigned in house that allows us to export metrics about GitLab application internals to prometheus. You can read more [in the project's readme](https://gitlab.com/gitlab-org/gitlab-monitor)
+
+### gitlab-workhorse
+
+- Omnibus configuration options
+- Layer: Core Service (Processor)
+
+[GitLab Workhorse](https://gitlab.com/gitlab-org/gitlab-workhorse) is a program designed at GitLab to help alieviate pressure from unicorn. You can read more about the [historical reasons for developing](https://about.gitlab.com/2016/04/12/a-brief-history-of-gitlab-workhorse/). It's designed to act as a smart reverse proxy to help speed up GitLab as a whole.
+
+### logrotate
+
+- [Omnibus configuration options](https://docs.gitlab.com/omnibus/settings/logs.html#logrotate)
+- Layer: Core Service
+
+GitLab is comprised of a large number of services that all log. We started bundling our own logrotate as of 7.4 to make sure we were logging responsibly. This is just a packaged version of the common opensource offering.
+
+### nginx
+
+- [Omnibus configuration options](https://docs.gitlab.com/omnibus/settings/nginx.html)
+- Layer: Core Service (Processor)
+
+Nginx as as an ingress port for all HTTP requests and routes them to the approriate sub-systems within GitLab. We are bundling an unmodified version of the popular open source webserver.
+
+### node-exporter
+
+- [Omnibus configuration options](https://docs.gitlab.com/ee/administration/monitoring/prometheus/node_exporter.html)
+- Layer: Monitoring
+
+[Node Exporter](https://github.com/prometheus/node_exporter) is a Prometheus tool that gives us metrics on the underlying machine. (Think CPU/Disk/Load) It's just a packaged version of the common open source offering from the Prometheus project.
+
+### postgres-exporter
+
+- [Omnibus configuration options](https://docs.gitlab.com/ee/administration/monitoring/prometheus/postgres_exporter.html)
+- Layer: Monitoring
+
+[Postgres-exporter](https://github.com/wrouesnel/postgres_exporter) is the community provided Prometheus exporter that will deliver data about Postgres to prometheus for use in Grafana Dashboards.
+
+### postgresql
+
+- [Omnibus configuration options](https://docs.gitlab.com/omnibus/settings/database.html)
+- Layer: Core Service (Data)
+
+GitLab packages the popular Database to provide storage for Application meta data and user information.
+
+### prometheus
+
+- [Omnibus configuration options](https://docs.gitlab.com/ee/administration/monitoring/prometheus/)
+- Layer: Monitoring
+
+Prometheus is a time-series tool that helps GitLab administrators expose metrics about the individual processes used to provide GitLab the service.
+
+### redis
+
+- [Omnibus configuration options](https://docs.gitlab.com/omnibus/settings/redis.html)
+- Layer: Core Service (Data)
+
+Redis is packaged to provide a place to store:
+
+- session data
+- temporary cache information
+- background job queues.
+
+### redis-exporter
+
+- [Omnibus configuration options](https://docs.gitlab.com/ee/administration/monitoring/prometheus/redis_exporter.html)
+- Layer: Monitoring
+
+[Redis Exporter](https://github.com/oliver006/redis_exporter) is designed to give specific metrics about the Redis process to Prometheus so that we can graph these metrics in Graphana.
+
+### sidekiq
+
+- Omnibus configuration options
+- Layer: Core Service (Processor)
+
+Sidekiq is a Ruby background job processor that pulls jobs from the redis queue and processes them. Background jobs allow GitLab to provide a faster request/response cycle by moving work into the background.
+
+### unicorn
+
+- [Omnibus configuration options](https://docs.gitlab.com/omnibus/settings/unicorn.html)
+- Layer: Core Service (Processor)
+
+[Unicorn](https://bogomips.org/unicorn/) is a Ruby application server that is used to run the core Rails Application that provides the user facing features in GitLab. Often process output you will see this as `bundle` or `config.ru` depending on the GitLab version.
+
+### Additional Processes
+
+### GitLab Pages
+
+TODO
+
+### Mattermost
+
+TODO
+
+## GitLab by Request Type
+
+GitLab provides two "interfaces" for end users to access the service:
+
+- Web HTTP Requests (Viewing the UI/API)
+- Git HTTP/SSH Requests (Pushing/Pulling Git Data)
+
+It's important to understand the distinction as some processes are used in both and others are exclusive to a specific request type.
+
+### GitLab Web HTTP Request Cycle
+
+When making a request to an HTTP Endpoint (Think `/users/sign_in`) the request will take the following path through the GitLab Service:
+
+- nginx - Acts as our first line reverse proxy
+- gitlab-workhorse - This determines if it needs to go to the Rails application or somewhere else to reduce load on unicorn.
+- unicorn - Since this is a web request, and it needs to access the application it will go to Unicorn.
+- Postgres/Gitaly/Redis - Depending on the type of request, it may hit these services to store or retreive data.
-**Sidekiq** is a worker that primarily handles sending out emails.
-It takes tasks from the Redis communication board;
-**A Unicorn worker** is a worker that handles quick/mundane tasks.
-They work with the communication board (Redis).
-Their job description:
- - check permissions by checking the user session stored in a Redis “cubby hole”;
- - make tasks for Sidekiq;
- - fetch stuff from the warehouse or move things around in there;
+### GitLab Git Request Cycle
-**GitLab-shell** is a third kind of worker that takes orders from a fax machine (SSH) instead of the front desk (HTTP).
-GitLab-shell communicates with Sidekiq via the “communication board” (Redis), and asks quick questions of the Unicorn workers either directly or via the front desk.
+Below we describe the different pathing that HTTP vs. SSH Git requests will take. There is some overlap with the Web Request Cycle but also some differences.
-**Gitaly** is a back desk that is specialized on reaching the disks to perform git operations efficiently and keep a copy of the result of costly operations. All git operations go through Gitaly.
+### Web Request (80/443)
+TODO
-**GitLab Enterprise Edition (the application)** is the collection of processes and business practices that the office is run by.
+### SSH Request (22)
+TODO
## System Layout
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/changelog.md b/doc/development/changelog.md
index f06d40d1dbb..cd0a1f46d27 100644
--- a/doc/development/changelog.md
+++ b/doc/development/changelog.md
@@ -133,15 +133,15 @@ If you're working on the GitLab EE repository, the entry will be added to
### Arguments
-| Argument | Shorthand | Purpose |
-| ----------------- | --------- | ---------------------------------------------------------------------------------------------------------- |
-| [`--amend`] | | Amend the previous commit |
-| [`--force`] | `-f` | Overwrite an existing entry |
-| [`--merge-request`] | `-m` | Set merge request ID |
-| [`--dry-run`] | `-n` | Don't actually write anything, just print |
-| [`--git-username`] | `-u` | Use Git user.name configuration as the author |
-| [`--type`] | `-t` | The category of the change, valid options are: added, fixed, changed, deprecated, removed, security, other |
-| [`--help`] | `-h` | Print help message |
+| Argument | Shorthand | Purpose |
+| ----------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------- |
+| [`--amend`] | | Amend the previous commit |
+| [`--force`] | `-f` | Overwrite an existing entry |
+| [`--merge-request`] | `-m` | Set merge request ID |
+| [`--dry-run`] | `-n` | Don't actually write anything, just print |
+| [`--git-username`] | `-u` | Use Git user.name configuration as the author |
+| [`--type`] | `-t` | The category of the change, valid options are: `added`, `fixed`, `changed`, `deprecated`, `removed`, `security`, `performance`, `other` |
+| [`--help`] | `-h` | Print help message |
[`--amend`]: #-amend
[`--force`]: #-force-or-f
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 f9e6efa2c30..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
@@ -171,7 +181,7 @@ There are a few gotchas with it:
class Base
def execute
return unless enabled?
-
+
# ...
# ...
end
@@ -185,12 +195,12 @@ There are a few gotchas with it:
class Base
def execute
return unless enabled?
-
+
do_something
end
-
+
private
-
+
def do_something
# ...
# ...
@@ -204,14 +214,14 @@ There are a few gotchas with it:
```ruby
module EE::Base
extend ::Gitlab::Utils::Override
-
+
override :do_something
def do_something
# Follow the above pattern to call super and extend it
end
end
```
-
+
This would require updating CE first, or make sure this is back ported to CE.
When prepending, place them in the `ee/` specific sub-directory, and
@@ -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
@@ -332,6 +343,21 @@ full implementation details.
[ce-mr-full-private]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12373
[ee-mr-full-private]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2199
+### Code in `config/routes`
+
+When we add `draw :admin` in `config/routes.rb`, the application will try to
+load the file located in `config/routes/admin.rb`, and also try to load the
+file located in `ee/config/routes/admin.rb`.
+
+In EE, it should at least load one file, at most two files. If it cannot find
+any files, an error will be raised. In CE, since we don't know if there will
+be an EE route, it will not raise any errors even if it cannot find anything.
+
+This means if we want to extend a particular CE route file, just add the same
+file located in `ee/config/routes`. If we want to add an EE only route, we
+could still put `draw :ee_only` in both CE and EE, and add
+`ee/config/routes/ee_only.rb` in EE, similar to `render_if_exists`.
+
### Code in `app/controllers/`
In controllers, the most common type of conflict is with `before_action` that
@@ -518,8 +544,6 @@ module API
end
end
- prepend EE::API::MergeRequests
-
params :optional_params do
# CE specific params go here...
@@ -527,6 +551,8 @@ module API
end
end
end
+
+API::MergeRequests.prepend(EE::API::MergeRequests)
```
And then we could override it in EE module:
@@ -567,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:
@@ -611,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])
@@ -624,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
@@ -661,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
@@ -693,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
@@ -713,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/feature_flags.md b/doc/development/feature_flags.md
index 350593cc813..1019a1fd0e2 100644
--- a/doc/development/feature_flags.md
+++ b/doc/development/feature_flags.md
@@ -33,7 +33,7 @@ You can follow the progress on that [in the issue on our issue tracker](https://
In general, it's better to have a group- or user-based gate, and you should prefer
it over the use of percentage gates. This would make debugging easier, as you
-filter for example logs and errors based on actors too. Futhermore, this allows
+filter for example logs and errors based on actors too. Furthermore, this allows
for enabling for the `gitlab-org` group first, while the rest of the users
aren't impacted.
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/migration_style_guide.md b/doc/development/migration_style_guide.md
index 6f31e5b82e5..e4e532bb4ed 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -187,12 +187,7 @@ end
When adding a foreign-key constraint to either an existing or new
column remember to also add a index on the column.
-This is _required_ if the foreign-key constraint specifies
-`ON DELETE CASCADE` or `ON DELETE SET NULL` behavior. On a cascading
-delete, the [corresponding record needs to be retrieved using an
-index](https://www.cybertec-postgresql.com/en/postgresql-indexes-and-foreign-keys/)
-(otherwise, we'd need to scan the whole table) for subsequent update or
-deletion.
+This is _required_ for all foreign-keys.
Here's an example where we add a new column with a foreign key
constraint. Note it includes `index: true` to create an index for it.
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 25c6371f3d7..1830641431e 100644
--- a/doc/development/testing_guide/review_apps.md
+++ b/doc/development/testing_guide/review_apps.md
@@ -1,74 +1,82 @@
-# Review apps
+# Review Apps
-We currently have review apps available as a manual job in EE pipelines. Here is
-[the first implementation](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6259).
-
-That said, [the Quality team is working](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6665)
-on making Review Apps automatically deployed by each pipeline, both in CE and EE.
+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 EE [pipeline][gitlab-pipeline] during the `test` stage, you can
- start the [`review` job][review-job]
+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 overridden with the images built and stored in the
+ [`CNG-mirror` project's registry][cng-mirror-registry].
+ - Since we're using [the official GitLab Helm chart][helm-chart], this means
+ you get a dedicated environment for your branch that's very close to what it
+ would look in production.
+1. Once the `review` 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 overridden][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
-
-[^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
+- 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.
## 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
@@ -76,6 +84,7 @@ on making Review Apps automatically deployed by each pipeline, both in CE and EE
[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..1b7e0d1d0ab 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
@@ -104,7 +104,7 @@ features of GitLab work with MySQL/MariaDB:
1. MySQL support for subgroups was [dropped with GitLab 9.3][post].
See [issue #30472][30472] for more information.
1. Geo does [not support MySQL](https://docs.gitlab.com/ee/administration/geo/replication/database.html#mysql-replication). This means no supported Disaster Recovery solution if using MySQL. **[PREMIUM ONLY]**
-1. [Zero downtime migrations][../update/README.md#upgrading-without-downtime] do not work with MySQL.
+1. [Zero downtime migrations](../update/README.md#upgrading-without-downtime) do not work with MySQL.
1. GitLab [optimizes the loading of dashboard events](https://gitlab.com/gitlab-org/gitlab-ce/issues/31806) using [PostgreSQL LATERAL JOINs](https://blog.heapanalytics.com/postgresqls-powerful-new-join-type-lateral/).
1. In general, SQL optimized for PostgreSQL may run much slower in MySQL due to
differences in query planners. For example, subqueries that work well in PostgreSQL
@@ -197,7 +197,13 @@ use the CI features.
## Supported web browsers
-We support the current and the previous major release of Firefox, Chrome/Chromium, Safari and Microsoft browsers (Microsoft Edge and Internet Explorer 11).
+We support the current and the previous major release of:
+
+- Firefox
+- Chrome/Chromium
+- Safari
+- Microsoft Edge
+- Internet Explorer 11
Each time a new browser version is released, we begin supporting that version and stop supporting the third most recent version.
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 9546f43eea8..394f3ea60b7 100644
--- a/doc/topics/authentication/index.md
+++ b/doc/topics/authentication/index.md
@@ -36,14 +36,14 @@ 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)
## Third-party resources
- [Kanboard Plugin GitLab Authentication](https://github.com/kanboard/plugin-gitlab-auth)
-- [Jenkins GitLab OAuth Plugin](https://wiki.jenkins-ci.org/display/JENKINS/GitLab+OAuth+Plugin)
+- [Jenkins GitLab OAuth Plugin](https://wiki.jenkins.io/display/JENKINS/GitLab+OAuth+Plugin)
- [Set up Gitlab CE with Active Directory authentication](https://www.caseylabs.com/setup-gitlab-ce-with-active-directory-authentication/)
- [How to customize GitLab to support OpenID authentication](http://eric.van-der-vlist.com/blog/2013/11/23/how-to-customize-gitlab-to-support-openid-authentication/)
- [Openshift - Configuring Authentication and User Agent](https://docs.openshift.org/latest/install_config/configuring_authentication.html#GitLab)
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 4d4832184e2..6bb2e236dc1 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -225,10 +225,11 @@ Auto DevOps at the project level.
### Enabling/disabling Auto DevOps at the project-level
-1. Check that your project doesn't have a `.gitlab-ci.yml`, or if one exists, remove it.
+If enabling, check that your project doesn't have a `.gitlab-ci.yml`, or if one exists, remove it.
+
1. Go to your project's **Settings > CI/CD > Auto DevOps**.
-1. Check the **Default to Auto DevOps pipeline** checkbox.
-1. Optionally, but recommended, add in the [base domain](#auto-devops-base-domain)
+1. Toggle the **Default to Auto DevOps pipeline** checkbox (checked to enable, unchecked to disable)
+1. When enabling, it's optional but recommended to add in the [base domain](#auto-devops-base-domain)
that will be used by Auto DevOps to [deploy your application](#auto-deploy)
and choose the [deployment strategy](#deployment-strategy).
1. Click **Save changes** for the changes to take effect.
@@ -246,12 +247,6 @@ There is also a feature flag to enable Auto DevOps to a percentage of projects
which can be enabled from the console with
`Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(10)`.
-### Disable Auto DevOps at the project level
-
-1. Go to your project's **Settings > CI/CD > Auto DevOps**.
-1. Uncheck the **Default to Auto DevOps pipeline** checkbox.
-1. Click **Save changes** for the changes to take effect.
-
### Deployment strategy
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/38542) in GitLab 11.0.
@@ -591,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`
@@ -662,10 +657,12 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac
| `REVIEW_DISABLED` | From GitLab 11.0, this variable can be used to disable the `review` and the manual `review:stop` job. If the variable is present, these jobs will not be created. |
| `DAST_DISABLED` | From GitLab 11.0, this variable can be used to disable the `dast` job. If the variable is present, the job will not be created. |
| `PERFORMANCE_DISABLED` | From GitLab 11.0, this variable can be used to disable the `performance` job. If the variable is present, the job will not be created. |
+| `OLD_REPORTS_DISABLED` | From GitLab 11.5, this variable can be used to disable the `sast` job. If the variable is present, the job will not be created. |
+| `NEW_REPORTS_DISABLED` | From GitLab 11.5, this variable can be used to disable the `sast_dashboard` job. If the variable is present, the job will not be created. |
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:**
@@ -743,7 +740,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.
@@ -756,7 +753,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/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md
index b12e86cb0a9..6326aadcdf2 100644
--- a/doc/topics/autodevops/quick_start_guide.md
+++ b/doc/topics/autodevops/quick_start_guide.md
@@ -83,6 +83,9 @@ under which this application will be deployed.
![GitLab GKE cluster details](img/guide_gitlab_gke_details.png)
1. Once ready, click **Create Kubernetes cluster**.
+
+NOTE: **Note:**
+Do not select `f1-micro` from the **Machine type** dropdown. `f1-micro` machines cannot support a full GitLab installation.
After a couple of minutes, the cluster will be created. You can also see its
status on your [GCP dashboard](https://console.cloud.google.com/kubernetes).
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 5edf92b3b09..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
@@ -205,7 +205,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
### 4. External Articles
-1. [2011 WSJ article by Marc Andreessen - Software is Eating the World](http://www.wsj.com/articles/SB10001424053111903480904576512250915629460)
+1. [2011 WSJ article by Marc Andreessen - Software is Eating the World](https://www.wsj.com/articles/SB10001424053111903480904576512250915629460)
1. [2014 Blog post by Chris Dixon - Software eats software development](http://cdixon.org/2014/04/13/software-eats-software-development/)
1. [2015 Venture Beat article - Actually, Open Source is Eating the World](http://venturebeat.com/2015/12/06/its-actually-open-source-software-thats-eating-the-world/)
@@ -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/bookclub/booklist.md b/doc/university/bookclub/booklist.md
index 26c3851276b..84b1f643b91 100644
--- a/doc/university/bookclub/booklist.md
+++ b/doc/university/bookclub/booklist.md
@@ -10,7 +10,7 @@ List of books and resources, that may be worth reading.
1. **The Humble Programmer**
- Edsger W. Dijkstra, 1972 ([paper](http://dl.acm.org/citation.cfm?id=361591))
+ Edsger W. Dijkstra, 1972 ([paper](https://dl.acm.org/citation.cfm?id=361591))
## Programming
diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md
index 6ff27e495fb..6e0f71017c6 100644
--- a/doc/university/glossary/README.md
+++ b/doc/university/glossary/README.md
@@ -303,7 +303,7 @@ A [tool](https://docs.gitlab.com/ee/integration/external-issue-tracker.html) use
### Jenkins
-An Open Source CI tool written using the Java programming language. [Jenkins](https://jenkins-ci.org/) does the same job as GitLab CI, Bamboo, and Travis CI. It is extremely popular. Related [documentation](https://docs.gitlab.com/ee/integration/jenkins.html).
+An Open Source CI tool written using the Java programming language. [Jenkins](https://jenkins.io/) does the same job as GitLab CI, Bamboo, and Travis CI. It is extremely popular. Related [documentation](https://docs.gitlab.com/ee/integration/jenkins.html).
### Jira
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..14c39457838 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)
+<https://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
new file mode 100644
index 00000000000..44105348d14
--- /dev/null
+++ b/doc/update/11.4-to-11.5.md
@@ -0,0 +1,390 @@
+---
+comments: false
+---
+
+# From 11.4 to 11.5
+
+Make sure you view this update guide 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 at 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/archives.html) for installation
+guide links by version.
+
+### 1. Stop server
+
+```bash
+sudo service gitlab stop
+```
+
+### 2. Backup
+
+NOTE: If you installed GitLab from source, make sure `rsync` is installed.
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Update Ruby
+
+NOTE: GitLab 11.0 and higher only support Ruby 2.4.x and dropped support for Ruby 2.3.x. Be
+sure to upgrade your interpreter if necessary.
+
+You can check which version you are running with `ruby -v`.
+
+Download Ruby and compile it:
+
+```bash
+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
+
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Update Node
+
+GitLab utilizes [webpack](http://webpack.js.org) to compile frontend assets.
+This requires a minimum version of node v6.0.0.
+
+You can check which version you are running with `node -v`. If you are running
+a version older than `v6.0.0` you will need to update to a newer version. You
+can find instructions to install from community maintained packages or compile
+from source at the nodejs.org website.
+
+<https://nodejs.org/en/download/>
+
+GitLab also requires the use of yarn `>= v1.2.0` to manage JavaScript
+dependencies.
+
+```bash
+curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
+echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
+sudo apt-get update
+sudo apt-get install yarn
+```
+
+More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install).
+
+### 5. Update Go
+
+NOTE: GitLab 11.4 and higher only supports Go 1.10.x and newer, and dropped support for Go
+1.9.x. Be sure to upgrade your installation if necessary.
+
+You can check which version you are running with `go version`.
+
+Download and install Go:
+
+```bash
+# Remove former Go installation folder
+sudo rm -rf /usr/local/go
+
+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.5.linux-amd64.tar.gz
+```
+
+### 6. Get latest code
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git fetch --all --prune
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+sudo -u git -H git checkout -- locale
+```
+
+For GitLab Community Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 11-5-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 11-5-stable-ee
+```
+
+### 7. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+
+sudo -u git -H git fetch --all --tags --prune
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION)
+sudo -u git -H bin/compile
+```
+
+### 8. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. GitLab-Workhorse uses
+[GNU Make](https://www.gnu.org/software/make/).
+If you are not using Linux you may have to run `gmake` instead of
+`make` below.
+
+```bash
+cd /home/git/gitlab-workhorse
+
+sudo -u git -H git fetch --all --tags --prune
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION)
+sudo -u git -H make
+```
+
+### 9. Update Gitaly
+
+#### Check Gitaly configuration
+
+Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly
+configuration file may contain syntax errors. The block name
+`[[storages]]`, which may occur more than once in your `config.toml`
+file, should be `[[storage]]` instead.
+
+```shell
+sudo -u git -H sed -i.pre-10.1 's/\[\[storages\]\]/[[storage]]/' /home/git/gitaly/config.toml
+```
+
+#### Compile Gitaly
+
+```shell
+cd /home/git/gitaly
+sudo -u git -H git fetch --all --tags --prune
+sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION)
+sudo -u git -H make
+```
+
+### 10. Update gitlab-pages
+
+#### Only needed if you use GitLab Pages.
+
+Install and compile gitlab-pages. GitLab-Pages uses
+[GNU Make](https://www.gnu.org/software/make/).
+If you are not using Linux you may have to run `gmake` instead of
+`make` below.
+
+```bash
+cd /home/git/gitlab-pages
+
+sudo -u git -H git fetch --all --tags --prune
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION)
+sudo -u git -H make
+```
+
+### 11. Update MySQL permissions
+
+If you are using MySQL you need to grant the GitLab user the necessary
+permissions on the database:
+
+```bash
+mysql -u root -p -e "GRANT TRIGGER ON \`gitlabhq_production\`.* TO 'git'@'localhost';"
+```
+
+If you use MySQL with replication, or just have MySQL configured with binary logging,
+you will need to also run the following on all of your MySQL servers:
+
+```bash
+mysql -u root -p -e "SET GLOBAL log_bin_trust_function_creators = 1;"
+```
+
+You can make this setting permanent by adding it to your `my.cnf`:
+
+```
+log_bin_trust_function_creators=1
+```
+
+### 12. Update configuration files
+
+#### New `unicorn.rb` configuration
+
+Note: we have made [changes](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22372) to `unicorn.rb` to allow GitLab run with both Unicorn and Puma in future.
+
+- Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/11-5-stable/config/unicorn.rb.example but with your settings.
+ - In particular, make sure that `require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events"` line exists and the `before_exec`, `before_fork`, and `after_fork` handlers are configured as shown below:
+
+```ruby
+require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events"
+
+before_exec do |server|
+ # Signal application hooks that we're about to restart
+ Gitlab::Cluster::LifecycleEvents.do_master_restart
+end
+
+before_fork do |server, worker|
+ # Signal application hooks that we're about to fork
+ Gitlab::Cluster::LifecycleEvents.do_before_fork
+end
+
+after_fork do |server, worker|
+ # Signal application hooks of worker start
+ Gitlab::Cluster::LifecycleEvents.do_worker_start
+end
+```
+
+#### New configuration options for `gitlab.yml`
+
+There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/11-4-stable:config/gitlab.yml.example origin/11-5-stable:config/gitlab.yml.example
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+cd /home/git/gitlab
+
+# For HTTPS configurations
+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-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
+configuration as GitLab application no longer handles setting it.
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-workhorse listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-5-stable/lib/support/init.d/gitlab.default.example#L38
+
+#### SMTP configuration
+
+If you're installing from source and use SMTP to deliver mail, you will need to add the following line
+to config/initializers/smtp_settings.rb:
+
+```ruby
+ActionMailer::Base.delivery_method = :smtp
+```
+
+See [smtp_settings.rb.sample] as an example.
+
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-5-stable/config/initializers/smtp_settings.rb.sample#L13
+
+#### Init script
+
+There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`:
+
+```sh
+cd /home/git/gitlab
+
+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:
+
+```bash
+cd /home/git/gitlab
+
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
+
+For Ubuntu 16.04.1 LTS:
+
+```bash
+sudo systemctl daemon-reload
+```
+
+### 13. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Optional: clean up old gems
+sudo -u git -H bundle clean
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Compile GetText PO files
+
+sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production
+
+# Update node dependencies and recompile assets
+sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production
+
+# Clean up cache
+sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
+```
+
+**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md).
+
+### 14. Start application
+
+```bash
+sudo service gitlab start
+sudo service nginx restart
+```
+
+### 15. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+```
+
+To make sure you didn't miss anything run a more thorough check:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+```
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (11.4)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 11.3 to 11.4](11.3-to-11.4.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
+
+[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-5-stable/config/gitlab.yml.example
+[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-5-stable/lib/support/init.d/gitlab.default.example
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/new_project.png b/doc/user/project/clusters/eks_and_gitlab/img/new_project.png
deleted file mode 100644
index 02afc099f10..00000000000
--- a/doc/user/project/clusters/eks_and_gitlab/img/new_project.png
+++ /dev/null
Binary files differ
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..517e4f7ca44
--- /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..fa2ed21f980 100644
--- a/doc/user/project/clusters/eks_and_gitlab/index.md
+++ b/doc/user/project/clusters/eks_and_gitlab/index.md
@@ -1,72 +1,171 @@
----
-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 to integrate an
+[Amazon EKS](https://aws.amazon.com/eks/) cluster with GitLab and begin
+deploying applications.
-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.
+## Introduction
For an end-to-end walkthrough we will:
-1. Start with a new project based on the sample Ruby on Rails template
-1. Integrate an EKS cluster
-1. Utilize [Auto DevOps](../../../../topics/autodevops/) to build, test, and deploy our application
+
+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. `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).
+1. An account on GitLab, like [GitLab.com](https://gitlab.com).
+1. An Amazon EKS cluster (with worker nodes properly configured).
+1. `kubectl` [installed and configured for access to the EKS cluster](https://docs.aws.amazon.com/eks/latest/userguide/getting-started.html#get-started-kubectl).
-## Creating a new project
+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).
-On GitLab, create a new project by clicking on the `+` icon in the top navigation bar, and selecting `New project`.
+## Creating a new project
-![New Project](img/new_project.png)
+On GitLab, create a new project by clicking on the `+` icon in the top navigation
+bar and selecting **New project**.
-On the new project screen, click on the `Create from template` tab, and select `Use template` for the Ruby on Rails sample project.
+On the new project screen, click on the **Create from template** tab, and select
+"Use template" for the Ruby on Rails sample project.
-Give the project a name, and then select `Create project`.
+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 > Kubernetes > Add Kubernetes cluster**,
+then click **Add an existing Kubernetes cluster**.
+
+A few details from the EKS cluster will be required to connect it to GitLab:
+
+1. **Retrieve the certificate**: A valid Kubernetes certificate is needed to
+ authenticate to the EKS cluster. We will use the certificate created by default.
+ Open a shell and use `kubectl` to retrieve it:
+
+ - List the secrets with `kubectl get secrets`, and one should named similar to
+ `default-token-xxxxx`. Copy that token name for use below.
+ - Get the certificate with:
+
+ ```sh
+ kubectl get secret <secret name> -o jsonpath="{['data']['ca\.crt']}" | base64 -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:
+
+ 2.1. Create a file called `eks-admin-service-account.yaml` with contents:
+
+ ```yaml
+ apiVersion: v1
+ kind: ServiceAccount
+ metadata:
+ name: eks-admin
+ namespace: kube-system
+ ```
+
+ 2.2. Apply the service account to your cluster:
+
+ ```bash
+ kubectl apply -f eks-admin-service-account.yaml
+ ```
+
+ Output:
-From the left side bar, hover over `Operations` and select `Kubernetes`, then click on `Add Kubernetes cluster`, and finally `Add an existing Kubernetes cluster`.
+ ```bash
+ serviceaccount "eks-admin" created
+ ```
-A few details from the EKS cluster will be required to connect it to GitLab.
+ 2.3. Create a file called `eks-admin-cluster-role-binding.yaml` with contents:
-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. 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.
+ ```yaml
+ apiVersion: rbac.authorization.k8s.io/v1beta1
+ kind: ClusterRoleBinding
+ metadata:
+ name: eks-admin
+ roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: cluster-admin
+ subjects:
+ - kind: ServiceAccount
+ name: eks-admin
+ namespace: kube-system
+ ```
+
+ 2.4. Apply the cluster role binding to your cluster:
+
+ ```bash
+ kubectl apply -f eks-admin-cluster-role-binding.yaml
+ ```
+
+ Output:
+
+ ```bash
+ clusterrolebinding "eks-admin" created
+ ```
+
+ 2.5. Retrieve the token for the `eks-admin` service account:
+
+ ```bash
+ kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep eks-admin | awk '{print $1}')
+ ```
+
+ Copy the `<authentication_token>` value from the output:
+
+ ```yaml
+ Name: eks-admin-token-b5zv4
+ Namespace: kube-system
+ Labels: <none>
+ Annotations: kubernetes.io/service-account.name=eks-admin
+ kubernetes.io/service-account.uid=bcfe66ac-39be-11e8-97e8-026dce96b6e8
+
+ Type: kubernetes.io/service-account-token
+
+ Data
+ ====
+ ca.crt: 1025 bytes
+ namespace: 11 bytes
+ token: <authentication_token>
+ ```
+
+1. The API server endpoint is also required, so GitLab can connect to the cluster.
+ This is displayed on the AWS EKS console, when viewing the EKS cluster details.
You now have all the information needed to connect the EKS cluster:
-* Kubernetes cluster name: Provide a name for the cluster to identify it within GitLab.
-* Environment scope: Leave this as `*` for now, since we are only connecting a single cluster.
-* API URL: Paste in the API server endpoint retrieved above.
-* CA Certificate: Paste the certificate data from the earlier step, as-is.
-* Paste the 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)
-Click on `Add Kubernetes cluster`, the cluster is now connected to GitLab. At this point, [Kubernetes deployment variables](../#deployment-variables) will automatically be available during CI jobs, making it easy to interact with the cluster.
+Click on **Add Kubernetes cluster**, the cluster is now connected to GitLab.
+At this point, [Kubernetes deployment variables](../#deployment-variables) will
+automatically be available during CI/CD jobs, making it easy to interact with the cluster.
If you would like to utilize your own CI/CD scripts to deploy to the cluster, you can stop here.
-## Disable Role-Based Access Control (RBAC)
+## 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.
+NOTE: **Note**: Disabling RBAC means that any application running in the cluster,
+or user who can authenticate to the cluster, has full API access. This is a
+[security concern](../index.md#security-implications), and may not be desirable.
To effectively disable RBAC, global permissions can be applied granting full access:
@@ -80,56 +179,100 @@ kubectl create clusterrolebinding permissive-binding \
## Deploy services to the cluster
-GitLab supports one-click deployment of helpful services to the cluster, many of which support Auto DevOps. Back on the Kubernetes cluster screen in GitLab, a list of applications is now available to deploy.
+GitLab supports one-click deployment of helpful services to the cluster, many of
+which support Auto DevOps. Back on the Kubernetes cluster screen in GitLab, a
+list of applications is now available to deploy.
-First install Helm Tiller, a package manager for Kubernetes. This enables deployment of the other applications.
+First, install Helm Tiller, a package manager for Kubernetes. This enables
+deployment of the other applications.
![Deploy Apps](img/deploy_apps.png)
### Deploying NGINX Ingress (optional)
-Next, if you would like the deployed app to be reachable on the internet, deploy the Ingress. Note that this will also cause an [Elastic Load Balancer](https://aws.amazon.com/documentation/elastic-load-balancing/) to be created, which will incur additional AWS costs.
+Next, if you would like the deployed app to be reachable on the internet, deploy
+the Ingress. Note that this will also cause an
+[Elastic Load Balancer](https://aws.amazon.com/documentation/elastic-load-balancing/)
+to be created, which will incur additional AWS costs.
-Once installed, you may see a `?` for `Ingress IP Address`. This is because the created ELB is available at a DNS name, not an IP address. To get the DNS name, run: `kubectl get service ingress-nginx-ingress-controller -n gitlab-managed-apps -o jsonpath="{.status.loadBalancer.ingress[0].hostname}"`. Note, you may see a trailing `%` on some Kubernetes versions, do not include it.
+Once installed, you may see a `?` for "Ingress IP Address". This is because the
+created ELB is available at a DNS name, not an IP address. To get the DNS name,
+run:
-The Ingress is now available at this address, and will route incoming requests to the proper service based on the DNS name in the request. To support this, a wildcard DNS CNAME record should be created for the desired domain name. For example `*.myekscluster.com` would point to the Ingress hostname obtained earlier.
+```sh
+kubectl get service ingress-nginx-ingress-controller -n gitlab-managed-apps -o jsonpath="{.status.loadBalancer.ingress[0].hostname}"
+```
+
+Note that you may see a trailing `%` on some Kubernetes versions, **do not include it**.
+
+The Ingress is now available at this address and will route incoming requests to
+the proper service based on the DNS name in the request. To support this, a
+wildcard DNS CNAME record should be created for the desired domain name. For example,
+`*.myekscluster.com` would point to the Ingress hostname obtained earlier.
![Create DNS](img/create_dns.png)
### Deploying the GitLab Runner (optional)
-If the project is on GitLab.com, free shared runners are available and you do not have to deploy one. If a project specific runner is desired, or there are no shared runners, it is easy to deploy one.
+If the project is on GitLab.com, free shared Runners are available and you do
+not have to deploy one. If a project specific Runner is desired, or there are no
+shared Runners, it is easy to deploy one.
-Simply click on the `Install` button for the GitLab Runner. It is important to note that the runner deployed is set as **privileged**, which means it essentially has root access to the underlying machine. This is required to build docker images, and so is on by default.
+Simply click on the **Install** button for the GitLab Runner. It is important to
+note that the Runner deployed is set as **privileged**, which means it essentially
+has root access to the underlying machine. This is required to build docker images,
+and so is on by default.
### Deploying Prometheus (optional)
-GitLab is able to monitor applications automatically, utilizing [Prometheus](../../integrations/prometheus.html). Kubernetes container CPU and memory metrics are automatically collected, and response metrics are retrieved from NGINX Ingress as well.
+GitLab is able to monitor applications automatically, utilizing
+[Prometheus](../../integrations/prometheus.html). Kubernetes container CPU and
+memory metrics are automatically collected, and response metrics are retrieved
+from NGINX Ingress as well.
-To enable monitoring, simply install Prometheus into the cluster with the `Install` button.
+To enable monitoring, simply install Prometheus into the cluster with the
+**Install** button.
## Create a default Storage Class
-Amazon EKS does not have a default Storage Class out of the box, which means requests for persistent volumes will not be automatically fulfilled. As part of Auto DevOps, the deployed Postgres instance requests persistent storage, and without a default storage class it will fail to start.
+Amazon EKS doesn't have a default Storage Class out of the box, which means
+requests for persistent volumes will not be automatically fulfilled. As part
+of Auto DevOps, the deployed Postgres instance requests persistent storage,
+and without a default storage class it will fail to start.
-If a default Storage Class does not already exist and is desired, follow Amazon's [short guide](https://docs.aws.amazon.com/eks/latest/userguide/storage-classes.html) to create one.
+If a default Storage Class doesn't already exist and is desired, follow Amazon's
+[guide on storage classes](https://docs.aws.amazon.com/eks/latest/userguide/storage-classes.html)
+to create one.
-Alternatively, disable Postgres by setting the project variable [`POSTGRES_ENABLED`](../../../../topics/autodevops/#environment-variables) to `false`.
+Alternatively, disable Postgres by setting the project variable
+[`POSTGRES_ENABLED`](../../../../topics/autodevops/#environment-variables) to `false`.
## Deploy the app to EKS
-With RBAC disabled and services deployed, [Auto DevOps](https://docs.gitlab.com/ee/topics/autodevops/) can now be leveraged to build, test, and deploy the app. To enable, click on `Settings` in the left sidebar, then `CI/CD`. You will see a section for `Auto DevOps`, expand it. Click on the radio button to `Enable Auto DevOps`.
+With RBAC disabled and services deployed,
+[Auto DevOps](../../../../topics/autodevops/index.md) can now be leveraged
+to build, test, and deploy the app.
-If a wildcard DNS entry was created resolving to the Load Balancer, enter it in the `domain` field. Otherwise, the deployed app will not be externally available outside of the cluster. To save, click `Save changes`.
+[Enable Auto DevOps](../../../../topics/autodevops/index.md##enablingdisabling-auto-devops-at-the-project-level)
+if not already enabled. If a wildcard DNS entry was created resolving to the
+Load Balancer, enter it in the `domain` field under the Auto DevOps settings.
+Otherwise, the deployed app will not be externally available outside of the cluster.
![Deploy Pipeline](img/pipeline.png)
-A new pipeline will automatically be created, which will begin to build, test, and deploy the app.
+A new pipeline will automatically be created, which will begin to build, test,
+and deploy the app.
-After the pipeline has finished, your app will be running in EKS and available to users. Click on `CI/CD` tab in the left navigation bar, and choose `Environments`.
+After the pipeline has finished, your app will be running in EKS and available
+to users. Click on **CI/CD > Environments**.
![Deployed Environment](img/environment.png)
-You will see a list of the environments and their deploy status, as well as options to browse to the app, view monitoring metrics, and even access a shell on the running pod.
+You will see a list of the environments and their deploy status, as well as
+options to browse to the app, view monitoring metrics, and even access a shell
+on the running pod.
+
+## Learn more
-To learn more about Auto DevOps, review our [documentation](../../../../topics/autodevops/).
+To learn more on automatically deploying your applications,
+read about [Auto DevOps](../../../../topics/autodevops/index.md).
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index 48004471f0a..ca262e4b76e 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -49,8 +49,8 @@ new Kubernetes cluster to your project:
NOTE: **Note:**
You need Maintainer [permissions] and above to access the Kubernetes page.
-1. Click on **Add Kubernetes cluster**.
-1. Click on **Create with Google Kubernetes Engine**.
+1. Click **Add Kubernetes cluster**.
+1. Click **Create with Google Kubernetes Engine**.
1. Connect your Google account if you haven't done already by clicking the
**Sign in with Google** button.
1. From there on, choose your cluster's settings:
@@ -78,8 +78,8 @@ To add an existing Kubernetes cluster to your project:
NOTE: **Note:**
You need Maintainer [permissions] and above to access the Kubernetes page.
-1. Click on **Add Kubernetes cluster**.
-1. Click on **Add an existing Kubernetes cluster** and fill in the details:
+1. Click **Add Kubernetes cluster**.
+1. Click **Add an existing Kubernetes cluster** and fill in the details:
- **Kubernetes cluster name** (required) - The name you wish to give the cluster.
- **Environment scope** (required)- The
[associated environment](#setting-the-environment-scope) to this cluster.
@@ -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,11 @@ 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) | 11.5+ | 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>`. This will require your kubernetes cluster to have [RBAC enabled](#role-based-access-control-rbac). | [knative/knative](https://storage.googleapis.com/triggermesh-charts)
+
+NOTE: **Note:**
+As of GitLab 11.6 Helm Tiller will be upgraded to the latest version supported
+by GitLab before installing any of the above applications.
## Getting the external IP address
@@ -224,6 +241,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.
@@ -238,14 +259,17 @@ your ingress application in which case you should manually determine it.
### Manually determining the IP address
-If the cluster is on GKE, click on the **Google Kubernetes Engine** link in the
+If the cluster is on GKE, click the **Google Kubernetes Engine** link in the
**Advanced settings**, or go directly to the
[Google Kubernetes Engine dashboard](https://console.cloud.google.com/kubernetes/)
-and select the proper project and cluster. Then click on **Connect** and execute
+and select the proper project and cluster. Then click **Connect** and execute
the `gcloud` command in a local terminal or using the **Cloud Shell**.
If the cluster is not on GKE, follow the specific instructions for your
Kubernetes provider to configure `kubectl` with the right credentials.
+The output of the following examples will show the external IP address of your
+cluster. This information can then be used to set up DNS entries and forwarding
+rules that allow external access to your deployed applications.
If you installed the Ingress [via the **Applications**](#installing-applications),
run the following command:
@@ -254,20 +278,23 @@ run the following command:
kubectl get svc --namespace=gitlab-managed-apps ingress-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip} '
```
-Otherwise, you can list the IP addresses of all load balancers:
+For Istio/Knative, the command will be different:
```bash
-kubectl get svc --all-namespaces -o jsonpath='{range.items[?(@.status.loadBalancer.ingress)]}{.status.loadBalancer.ingress[*].ip} '
+kubectl get svc --namespace=istio-system knative-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip} '
```
-> **Note**: Some Kubernetes clusters return a hostname instead, like [Amazon EKS](https://aws.amazon.com/eks/). For these platforms, run:
-> ```bash
-> kubectl get service ingress-nginx-ingress-controller -n gitlab-managed-apps -o jsonpath="{.status.loadBalancer.ingress[0].hostname}".
-> ```
+Some Kubernetes clusters return a hostname instead, like [Amazon EKS](https://aws.amazon.com/eks/). For these platforms, run:
-The output is the external IP address of your cluster. This information can then
-be used to set up DNS entries and forwarding rules that allow external access to
-your deployed applications.
+```bash
+kubectl get service ingress-nginx-ingress-controller -n gitlab-managed-apps -o jsonpath="{.status.loadBalancer.ingress[0].hostname}".
+```
+
+Otherwise, you can list the IP addresses of all load balancers:
+
+```bash
+kubectl get svc --all-namespaces -o jsonpath='{range.items[?(@.status.loadBalancer.ingress)]}{.status.loadBalancer.ingress[*].ip} '
+```
### Using a static IP
@@ -277,7 +304,7 @@ your apps will not be able to be reached, and you'd have to change the DNS
record again. In order to avoid that, you should change it into a static
reserved IP.
-[Read how to promote an ephemeral external IP address in GKE.](https://cloud.google.com/compute/docs/ip-addresses/reserve-static-external-ip-address#promote_ephemeral_ip)
+Read how to [promote an ephemeral external IP address in GKE](https://cloud.google.com/compute/docs/ip-addresses/reserve-static-external-ip-address#promote_ephemeral_ip).
### Pointing your DNS at the cluster IP
@@ -368,18 +395,22 @@ 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
Kubernetes cluster integration:
-1. Click the "Enabled/Disabled" switch
+1. Click the **Enabled/Disabled** switch
1. Hit **Save** for the changes to take effect
You can now start using your Kubernetes cluster for your deployments.
@@ -396,7 +427,7 @@ When you remove a cluster, you only remove its relation to GitLab, not the
cluster itself. To remove the cluster, you can do so by visiting the GKE
dashboard or using `kubectl`.
-To remove the Kubernetes cluster integration from your project, simply click on the
+To remove the Kubernetes cluster integration from your project, simply click the
**Remove integration** button. You will then be able to follow the procedure
and add a Kubernetes cluster again.
@@ -459,7 +490,13 @@ the deployment variables above, ensuring any pods you create are labelled with
## Read more
-- [Connecting and deploying to an Amazon EKS cluster](eks_and_gitlab/index.md)
+### Integrating Amazon EKS cluster with GitLab
+
+- Learn how to [connect and deploy to an Amazon EKS cluster](eks_and_gitlab/index.md).
+
+### Serverless
+
+- [Run serverless workloads on Kubernetes with Knative.](serverless/index.md)
[permissions]: ../../permissions.md
[ee]: https://about.gitlab.com/pricing/
diff --git a/doc/user/project/clusters/runbooks/img/authorize-jupyter.png b/doc/user/project/clusters/runbooks/img/authorize-jupyter.png
new file mode 100644
index 00000000000..64f95ed45f0
--- /dev/null
+++ b/doc/user/project/clusters/runbooks/img/authorize-jupyter.png
Binary files differ
diff --git a/doc/user/project/clusters/runbooks/img/demo-runbook.png b/doc/user/project/clusters/runbooks/img/demo-runbook.png
new file mode 100644
index 00000000000..25c9df4126d
--- /dev/null
+++ b/doc/user/project/clusters/runbooks/img/demo-runbook.png
Binary files differ
diff --git a/doc/user/project/clusters/runbooks/img/gitlab-variables.png b/doc/user/project/clusters/runbooks/img/gitlab-variables.png
new file mode 100644
index 00000000000..f76ed21145f
--- /dev/null
+++ b/doc/user/project/clusters/runbooks/img/gitlab-variables.png
Binary files differ
diff --git a/doc/user/project/clusters/runbooks/img/helm-install.png b/doc/user/project/clusters/runbooks/img/helm-install.png
new file mode 100644
index 00000000000..e39094bcbf7
--- /dev/null
+++ b/doc/user/project/clusters/runbooks/img/helm-install.png
Binary files differ
diff --git a/doc/user/project/clusters/runbooks/img/ingress-install.png b/doc/user/project/clusters/runbooks/img/ingress-install.png
new file mode 100644
index 00000000000..093c61f2d0e
--- /dev/null
+++ b/doc/user/project/clusters/runbooks/img/ingress-install.png
Binary files differ
diff --git a/doc/user/project/clusters/runbooks/img/jupyterhub-install.png b/doc/user/project/clusters/runbooks/img/jupyterhub-install.png
new file mode 100644
index 00000000000..2115ec9745b
--- /dev/null
+++ b/doc/user/project/clusters/runbooks/img/jupyterhub-install.png
Binary files differ
diff --git a/doc/user/project/clusters/runbooks/img/postgres-query.png b/doc/user/project/clusters/runbooks/img/postgres-query.png
new file mode 100644
index 00000000000..3880438c97a
--- /dev/null
+++ b/doc/user/project/clusters/runbooks/img/postgres-query.png
Binary files differ
diff --git a/doc/user/project/clusters/runbooks/img/sample-runbook.png b/doc/user/project/clusters/runbooks/img/sample-runbook.png
new file mode 100644
index 00000000000..c12ce8990a4
--- /dev/null
+++ b/doc/user/project/clusters/runbooks/img/sample-runbook.png
Binary files differ
diff --git a/doc/user/project/clusters/runbooks/index.md b/doc/user/project/clusters/runbooks/index.md
new file mode 100644
index 00000000000..e1b8dc07b50
--- /dev/null
+++ b/doc/user/project/clusters/runbooks/index.md
@@ -0,0 +1,137 @@
+# Runbooks
+
+Runbooks are a collection of documented procedures that explain how to
+carry out a particular process, be it starting, stopping, debugging,
+or troubleshooting a particular system.
+
+## Overview
+
+Historically, runbooks took the form of a decision tree or a detailed
+step-by-step guide depending on the condition or system.
+
+Modern implementations have introduced the concept of an "executable
+runbooks", where, along with a well-defined process, operators can execute
+pre-written code blocks or database queries against a given environment.
+
+## Nurtch Executable Runbooks
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/45912) in GitLab 11.4.
+
+The JupyterHub app offered via GitLab’s Kubernetes integration now ships
+with Nurtch’s Rubix library, providing a simple way to create DevOps
+runbooks. A sample runbook is provided, showcasing common operations.
+
+**<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+Watch this [video](https://www.youtube.com/watch?v=Q_OqHIIUPjE)
+for an overview of how this is acomplished in GitLab!**
+
+## Requirements
+
+To create an executable runbook, you will need:
+
+1. **Kubernetes** - A Kubernetes cluster is required to deploy the rest of the applications.
+ The simplest way to get started is to add a cluster using [GitLab's GKE integration](https://docs.gitlab.com/ee/user/project/clusters/#adding-and-creating-a-new-gke-cluster-via-gitlab).
+1. **Helm Tiller** - Helm is a package manager for Kubernetes and is required to install
+ all the other applications. It is installed in its own pod inside the cluster which
+ can run the helm CLI in a safe environment.
+1. **Ingress** - Ingress can provide load balancing, SSL termination, and name-based
+ virtual hosting. It acts as a web proxy for your applications.
+1. **JupyterHub** - JupyterHub is a multi-user service for managing notebooks across
+ a team. Jupyter Notebooks provide a web-based interactive programming environment
+ used for data analysis, visualization, and machine learning.
+
+## Nurtch
+
+Nurtch is the company behind the [Rubix library](https://github.com/Nurtch/rubix). Rubix is
+an open-source python library that makes it easy to perform common DevOps tasks inside Jupyter Notebooks.
+Tasks such as plotting Cloudwatch metrics and rolling your ECS/Kubernetes app are simplified
+down to a couple of lines of code. See the [Nurtch Documentation](http://docs.nurtch.com/en/latest)
+for more information.
+
+## Configure an executable runbook with GitLab
+
+Follow this step-by-step guide to configure an executable runbook in GitLab using
+the components outlined above and the preloaded demo runbook.
+
+### 1. Add a Kubernetes cluster
+
+Follow the steps outlined in [Adding and creating a new GKE cluster via GitLab](https://docs.gitlab.com/ee/user/project/clusters/#adding-and-creating-a-new-gke-cluster-via-gitlab)
+to add a Kubernetes cluster to your project.
+
+### 2. Install Helm Tiller, Ingress, and JupyterHub
+
+Once the cluster has been provisioned in GKE, click the **Install** button next to the **Helm Tiller** app.
+
+![install helm](img/helm-install.png)
+
+Once Tiller has been installed successfully, click the **Install** button next to the **Ingress** app.
+
+![install ingress](img/ingress-install.png)
+
+Once Ingress has been installed successfully, click the **Install** button next to the **JupyterHub** app.
+
+![install jupyterhub](img/jupyterhub-install.png)
+
+### 3. Login to JupyterHub and start the server
+
+Once JupyterHub has been installed successfully, navigate to the displayed **Jupyter Hostname** URL and click
+**Sign in with GitLab**. Authentication is automatically enabled for any user of the GitLab instance via OAuth2. This
+will redirect to GitLab in order to authorize JupyterHub to use your GitLab account. Click **Authorize**.
+
+![authorize jupyter](img/authorize-jupyter.png)
+
+Once the application has been authorized you will taken back to the JupyterHub application. Click **Start My Server**.
+The server will take a couple of seconds to start.
+
+### 4. Configure access
+
+In order for the runbook to access your GitLab project, you will need to enter a
+[GitLab Access Token](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html)
+as well as your Project ID in the **Setup** section of the demo runbook.
+
+Double-click the **DevOps-Runbook-Demo** folder located on the left panel.
+
+![demo runbook](img/demo-runbook.png)
+
+Double-click the "Nurtch-DevOps-Demo.ipynb" runbook.
+
+![sample runbook](img/sample-runbook.png)
+
+The contents on the runbook will be displayed on the right side of the screen. Under the "Setup" section, you will find
+entries for both your `PRIVATE_TOKEN` and your `PROJECT_ID`. Enter both these values, conserving the single quotes as follows:
+
+```sql
+PRIVATE_TOKEN = 'n671WNGecHugsdEDPsyo'
+PROJECT_ID = '1234567'
+```
+
+Update the `VARIABLE_NAME` on the last line of this section to match the name of the variable you are using for your
+access token. In this example our variable name is `PRIVATE_TOKEN`.
+
+```sql
+VARIABLE_VALUE = project.variables.get('PRIVATE_TOKEN').value
+```
+
+### 5. Configure an operation
+
+For this example we'll use the "**Run SQL queries in Notebook**" section in the sample runbook to query
+a postgres database. The first 4 lines of the section define the variables that are required for this query to function.
+
+```sql
+%env DB_USER={project.variables.get('DB_USER').value}
+%env DB_PASSWORD={project.variables.get('DB_PASSWORD').value}
+%env DB_ENDPOINT={project.variables.get('DB_ENDPOINT').value}
+%env DB_NAME={project.variables.get('DB_NAME').value}
+```
+
+Create the matching variables in your project's **Settings >> CI/CD >> Variables**
+
+![gitlab variables](img/gitlab-variables.png)
+
+Back in Jupyter, click the "Run SQL queries in Notebook" heading and the click *Run*. The results will be
+displayed in-line as follows:
+
+![postgres query](img/postgres-query.png)
+
+You can try other operations such as running shell scripts or interacting with a Kubernetes cluster. Visit the
+[Nurtch Documentation](http://docs.nurtch.com/) for more information. \ No newline at end of file
diff --git a/doc/user/project/clusters/serverless/img/deploy-stage.png b/doc/user/project/clusters/serverless/img/deploy-stage.png
new file mode 100644
index 00000000000..dc2f8af9c63
--- /dev/null
+++ b/doc/user/project/clusters/serverless/img/deploy-stage.png
Binary files differ
diff --git a/doc/user/project/clusters/serverless/img/dns-entry.png b/doc/user/project/clusters/serverless/img/dns-entry.png
new file mode 100644
index 00000000000..2e7655c6041
--- /dev/null
+++ b/doc/user/project/clusters/serverless/img/dns-entry.png
Binary files differ
diff --git a/doc/user/project/clusters/serverless/img/install-knative.png b/doc/user/project/clusters/serverless/img/install-knative.png
new file mode 100644
index 00000000000..dd576a9df35
--- /dev/null
+++ b/doc/user/project/clusters/serverless/img/install-knative.png
Binary files differ
diff --git a/doc/user/project/clusters/serverless/img/knative-app.png b/doc/user/project/clusters/serverless/img/knative-app.png
new file mode 100644
index 00000000000..54301e1786f
--- /dev/null
+++ b/doc/user/project/clusters/serverless/img/knative-app.png
Binary files differ
diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md
new file mode 100644
index 00000000000..bdbc4f7f09d
--- /dev/null
+++ b/doc/user/project/clusters/serverless/index.md
@@ -0,0 +1,137 @@
+# Serverless
+
+> Introduced in GitLab 11.5.
+
+Run serverless workloads on Kubernetes using [Knative](https://cloud.google.com/knative/).
+
+## Overview
+
+Knative extends Kubernetes to provide a set of middleware components that are useful to build modern, source-centric, container-based applications. Knative brings some significant benefits out of the box through its main components:
+
+- [Build:](https://github.com/knative/build) Source-to-container build orchestration
+- [Eventing:](https://github.com/knative/eventing) Management and delivery of events
+- [Serving:](https://github.com/knative/serving) Request-driven compute that can scale to zero
+
+For more information on Knative, visit the [Knative docs repo](https://github.com/knative/docs).
+
+## Requirements
+
+To run Knative on Gitlab, you will need:
+
+1. **Kubernetes:** An RBAC-enabled Kubernetes cluster is required to deploy Knative.
+ The simplest way to get started is to add a cluster using [GitLab's GKE integration](https://docs.gitlab.com/ee/user/project/clusters/#adding-and-creating-a-new-gke-cluster-via-gitlab).
+ GitLab recommends
+1. **Helm Tiller:** Helm is a package manager for Kubernetes and is required to install
+ all the other applications.
+1. **Domain Name:** Knative will provide its own load balancer using Istio. It will provide an
+ external IP address for all the applications served by Knative. You will be prompted to enter a
+ wildcard domain where your applications will be served. Configure your DNS server to use the
+ external IP address for that domain.
+1. **Serverless `gitlab-ci.yml` Template:** GitLab uses [Kaniko](https://github.com/GoogleContainerTools/kaniko)
+ to build the application and the [TriggerMesh CLI](https://github.com/triggermesh/tm), to simplify the
+ deployment of knative services and functions.
+
+ Add the following `.gitlab-ci.yml` to the root of your repository (you may skip this step if using the sample
+ [Knative Ruby App](https://gitlab.com/knative-examples/knative-ruby-app) mentioned below).
+
+ ```yaml
+ stages:
+ - build
+ - deploy
+
+ build:
+ stage: build
+ image:
+ name: gcr.io/kaniko-project/executor:debug
+ entrypoint: [""]
+ only:
+ - master
+ script:
+ - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
+ - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE
+
+ deploy:
+ stage: deploy
+ image: gcr.io/triggermesh/tm@sha256:e3ee74db94d215bd297738d93577481f3e4db38013326c90d57f873df7ab41d5
+ only:
+ - master
+ environment: production
+ script:
+ - echo "$CI_REGISTRY_IMAGE"
+ - tm -n "$KUBE_NAMESPACE" --config "$KUBECONFIG" deploy service "$CI_PROJECT_NAME" --from-image "$CI_REGISTRY_IMAGE" --wait
+ ```
+
+1. **Dockerfile:** Knative requires a Dockerfile in order to build your application. It should be included
+ at the root of your project's repo and expose port 8080.
+
+## Installing Knative via GitLab's Kubernetes integration
+
+NOTE: **Note:**
+Minimum recommended cluster size to run Knative is 3-nodes, 6 vCPUs, and 22.50 GB memory. RBAC must be enabled.
+
+You may download the sample [Knative Ruby App](https://gitlab.com/knative-examples/knative-ruby-app) to get started.
+
+1. [Add a Kubernetes cluster](https://docs.gitlab.com/ce/user/project/clusters/) and install Helm.
+
+1. Once Helm has been successfully installed, on the Knative app section, enter the domain to be used with
+ your application and click "Install".
+
+ ![install-knative](img/install-knative.png)
+
+1. After the Knative installation has finished, retrieve the Istio Ingress IP address by running the following command:
+
+ ```bash
+ kubectl get svc --namespace=istio-system knative-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip} '
+ ```
+
+ Output:
+
+ ```bash
+ 35.161.143.124 my-machine-name:~ my-user$
+ ```
+
+1. The ingress is now available at this address and will route incoming requests to the proper service based on the DNS
+ name in the request. To support this, a wildcard DNS A record should be created for the desired domain name. For example,
+ if your Knative base domain is `knative.example.com` then you need to create an A record with domain `*.knative.example.com`
+ pointing the ip address of the ingress.
+
+ ![dns entry](img/dns-entry.png)
+
+## Deploy the application with Knative
+
+With all the pieces in place, you can simply create a new CI pipeline to deploy the Knative application. Navigate to
+**CI/CD >> Pipelines** and click the **Run Pipeline** button at the upper-right part of the screen. Then, on the
+Pipelines page, click **Create pipeline**.
+
+## Obtain the URL for the Knative deployment
+
+Once all the stages of the pipeline finish, click the **deploy** stage.
+
+![deploy stage](img/deploy-stage.png)
+
+The output will look like this:
+
+```bash
+Running with gitlab-runner 11.5.0~beta.844.g96d88322 (96d88322)
+ on docker-auto-scale 72989761
+Using Docker executor with image gcr.io/triggermesh/tm@sha256:e3ee74db94d215bd297738d93577481f3e4db38013326c90d57f873df7ab41d5 ...
+Pulling docker image gcr.io/triggermesh/tm@sha256:e3ee74db94d215bd297738d93577481f3e4db38013326c90d57f873df7ab41d5 ...
+Using docker image sha256:6b3f6590a9b30bd7aafb9573f047d930c70066e43955b4beb18a1eee175f6de1 for gcr.io/triggermesh/tm@sha256:e3ee74db94d215bd297738d93577481f3e4db38013326c90d57f873df7ab41d5 ...
+Running on runner-72989761-project-4342902-concurrent-0 via runner-72989761-stg-srm-1541795796-27929c96...
+Cloning repository...
+Cloning into '/builds/danielgruesso/knative'...
+Checking out 8671ad20 as master...
+Skipping Git submodules setup
+$ echo "$CI_REGISTRY_IMAGE"
+registry.staging.gitlab.com/danielgruesso/knative
+$ tm -n "$KUBE_NAMESPACE" --config "$KUBECONFIG" deploy service "$CI_PROJECT_NAME" --from-image "$CI_REGISTRY_IMAGE" --wait
+Deployment started. Run "tm -n knative-4342902 describe service knative" to see the details
+Waiting for ready state.......
+Service domain: knative.knative-4342902.knative.info
+Job succeeded
+```
+
+The second to last line, labeled **Service domain** contains the URL for the deployment. Copy and paste the domain into your
+browser to see the app live.
+
+![knative app](img/knative-app.png) \ No newline at end of file
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..42da2210fab 100644
--- a/doc/user/project/import/github.md
+++ b/doc/user/project/import/github.md
@@ -17,6 +17,7 @@ the [GitHub rake task](../../../administration/raketasks/github_import.md) to im
GitHub without the constraints of a Sidekiq worker.
The following aspects of a project are imported:
+
* Repository description (GitLab.com & 7.7+)
* Git repository data (GitLab.com & 7.7+)
* Issues (GitLab.com & 7.7+)
@@ -65,9 +66,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 +78,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 +93,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 +108,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/issue_board.md b/doc/user/project/issue_board.md
index 464fa5987c1..9e2434c02ec 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -176,6 +176,9 @@ Clicking on the current board name in the upper left corner will reveal a
menu from where you can create another Issue Board and rename or delete the
existing one.
+When you're revisiting an issue board in a project or group with multiple boards,
+GitLab will automatically load the last board you visited.
+
NOTE: **Note:**
The Multiple Issue Boards feature is available for
**projects in GitLab Starter Edition** and for **groups in GitLab Premium Edition**.
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/img/comment-on-any-diff-line.png b/doc/user/project/merge_requests/img/comment-on-any-diff-line.png
new file mode 100644
index 00000000000..856ede41527
--- /dev/null
+++ b/doc/user/project/merge_requests/img/comment-on-any-diff-line.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/merge_request_diff_file_navigation.png b/doc/user/project/merge_requests/img/merge_request_diff_file_navigation.png
index 3b3bf88df31..1cdac5ef573 100644
--- a/doc/user/project/merge_requests/img/merge_request_diff_file_navigation.png
+++ b/doc/user/project/merge_requests/img/merge_request_diff_file_navigation.png
Binary files differ
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index f9ebf277125..044bc4c575b 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -141,6 +141,15 @@ you hide discussions that are no longer relevant.
[Read more about resolving discussion comments in merge requests reviews.](../../discussions/index.md)
+## Commenting on any file line in merge requests
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/13950) in GitLab 11.5.
+
+GitLab provides a way of leaving comments in any part of the file being changed
+in a Merge Request. To do so, click the **...** button in the gutter of the Merge Request diff UI to expand the diff lines and leave a comment, just as you would for a changed line.
+
+![Comment on any diff file line](img/comment-on-any-diff-line.png)
+
## Resolve conflicts
When a merge request has conflicts, GitLab may provide the option to resolve
@@ -166,9 +175,26 @@ 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
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22723) in GitLab 11.5.
+
+You can add commits to the merge request being created by adding
+patches as attachments to the email. All attachments with a filename
+ending in `.patch` will be considered patches and they will be processed
+ordered by name.
+
+The combined size of the patches can be 2MB.
+
+If the source branch from the subject does not exist, it will be
+created from the repository's HEAD or the specified target branch to
+apply the patches. The target branch can be specified using the
+[`/target_branch` quick action](../quick_actions.md). If the source
+branch already exists, the patches will be applied on top of it.
+
## 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).
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/2383) in GitLab 10.5.
When viewing the commit details page, GitLab will link to the merge request (or
merge requests, if it's in more than one) containing that commit.
@@ -205,9 +231,10 @@ have been marked as a **Work In Progress**.
## Merge request diff file navigation
-The diff view has a file tree for file navigation. As you scroll through
-diffs with a large number of files, you can easily jump to any changed file
-using the file tree.
+When reviewing changes in the **Changes** tab the diff can be navigated using
+the file tree or file list. As you scroll through large diffs with many
+changes, you can quickly jump to any changed file using the file tree or file
+list.
![Merge request diff file navigation](img/merge_request_diff_file_navigation.png)
@@ -236,6 +263,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/milestones/index.md b/doc/user/project/milestones/index.md
index 3cf46231a9d..7168fe63887 100644
--- a/doc/user/project/milestones/index.md
+++ b/doc/user/project/milestones/index.md
@@ -6,6 +6,26 @@ Milestones in GitLab are a way to track issues and merge requests created to ach
Milestones allow you to organize issues and merge requests into a cohesive group, with an optional start date and an optional due date.
+## Milestones as Agile sprints
+
+Milestones can be used as Agile sprints.
+Set the milestone start date and due date to represent
+the start and end of your Agile sprint.
+Set the milestone title to the name of your Agile sprint,
+such as `November 2018 sprint`.
+Add an issue to your Agile sprint by associating
+the milestone to the issue.
+
+## Milestones as releases
+
+Milestones can be used as releases.
+Set the milestone due date to represent the release date of your release.
+(And leave the milestone start date blank.)
+Set the the milestone title to the version of your release,
+such as `Version 9.4`.
+Add an issue to your release by associating
+the milestone to the issue.
+
## Project milestones and group milestones
- **Project milestones** can be assigned to issues or merge requests in that project only.
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/pages/getting_started_part_three.md b/doc/user/project/pages/getting_started_part_three.md
index e1eede8bbed..89b9621b8b9 100644
--- a/doc/user/project/pages/getting_started_part_three.md
+++ b/doc/user/project/pages/getting_started_part_three.md
@@ -212,7 +212,7 @@ security measure, necessary just for big companies, like banks and shoppings sit
with financial transactions.
Now we have a different picture. [According to Josh Aas](https://letsencrypt.org/2015/10/29/phishing-and-malware.html), Executive Director at [ISRG](https://en.wikipedia.org/wiki/Internet_Security_Research_Group):
-> _We’ve since come to realize that HTTPS is important for almost all websites. It’s important for any website that allows people to log in with a password, any website that [tracks its users](https://www.washingtonpost.com/news/the-switch/wp/2013/12/10/nsa-uses-google-cookies-to-pinpoint-targets-for-hacking/) in any way, any website that [doesn’t want its content altered](http://arstechnica.com/tech-policy/2014/09/why-comcasts-javascript-ad-injections-threaten-security-net-neutrality/), and for any site that offers content people might not want others to know they are consuming. We’ve also learned that any site not secured by HTTPS [can be used to attack other sites](http://krebsonsecurity.com/2015/04/dont-be-fodder-for-chinas-great-cannon/)._
+> _We’ve since come to realize that HTTPS is important for almost all websites. It’s important for any website that allows people to log in with a password, any website that [tracks its users](https://www.washingtonpost.com/news/the-switch/wp/2013/12/10/nsa-uses-google-cookies-to-pinpoint-targets-for-hacking/) in any way, any website that [doesn’t want its content altered](http://arstechnica.com/tech-policy/2014/09/why-comcasts-javascript-ad-injections-threaten-security-net-neutrality/), and for any site that offers content people might not want others to know they are consuming. We’ve also learned that any site not secured by HTTPS [can be used to attack other sites](https://krebsonsecurity.com/2015/04/dont-be-fodder-for-chinas-great-cannon/)._
Therefore, the reason why certificates are so important is that they encrypt
the connection between the **client** (you, me, your visitors)
diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md
index 0a4542b71ed..85a03d125dd 100644
--- a/doc/user/project/quick_actions.md
+++ b/doc/user/project/quick_actions.md
@@ -53,6 +53,7 @@ discussions, and descriptions:
| `/target_branch <Local branch Name>` | Set target branch | | ✓ |
| `/wip` | Toggle the Work In Progress status | | ✓ |
| `/merge` | Merge (when pipeline succeeds) | | ✓ |
+| `/create_merge_request <branch name>` | Create a new merge request starting from the current issue | ✓ | |
## Quick actions for commit messages
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/user/project/settings/index.md b/doc/user/project/settings/index.md
index 084d1161633..d6754372816 100644
--- a/doc/user/project/settings/index.md
+++ b/doc/user/project/settings/index.md
@@ -18,6 +18,8 @@ Adjust your project's name, description, avatar, [default branch](../repository/
![general project settings](img/general_settings.png)
+The project description also partially supports [standard markdown](../../markdown.md#standard-markdown). You can use [emphasis](../../markdown.md#emphasis), [links](../../markdown.md#links), and [line-breaks](../../markdown.md#line-breaks) to add more context to the project description.
+
### Sharing and permissions
Set up your project's access, [visibility](../../../public_access/public_access.md), and enable [Container Registry](../container_registry.md) for your projects:
diff --git a/doc/user/search/img/dashboard_links.png b/doc/user/search/img/dashboard_links.png
new file mode 100644
index 00000000000..2c472c7e464
--- /dev/null
+++ b/doc/user/search/img/dashboard_links.png
Binary files differ
diff --git a/doc/user/search/img/issues_assigned_to_you.png b/doc/user/search/img/issues_assigned_to_you.png
index 36c670eedd5..d2fff5e9a67 100644
--- a/doc/user/search/img/issues_assigned_to_you.png
+++ b/doc/user/search/img/issues_assigned_to_you.png
Binary files differ
diff --git a/doc/user/search/img/issues_filter_none_any.png b/doc/user/search/img/issues_filter_none_any.png
new file mode 100644
index 00000000000..9682fc55315
--- /dev/null
+++ b/doc/user/search/img/issues_filter_none_any.png
Binary files differ
diff --git a/doc/user/search/img/left_menu_bar.png b/doc/user/search/img/left_menu_bar.png
deleted file mode 100644
index d68a71cba8e..00000000000
--- a/doc/user/search/img/left_menu_bar.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/search/index.md b/doc/user/search/index.md
index 4f1b96b775c..78c1294346b 100644
--- a/doc/user/search/index.md
+++ b/doc/user/search/index.md
@@ -2,27 +2,27 @@
## Issues and merge requests
-To search through issues and merge requests in multiple projects, you can use the left-sidebar.
+To search through issues and merge requests in multiple projects, you can use the **Issues** or **Merge Requests** links
+in the top-right part of your screen.
-Click the menu bar, then **Issues** or **Merge Requests**, which work in the same way,
-therefore, the following notes are valid for both.
+Both of them work in the same way, therefore, the following notes are valid for both.
The number displayed on their right represents the number of issues and merge requests assigned to you.
-![menu bar - issues and MRs assigned to you](img/left_menu_bar.png)
+![issues and MRs dashboard links](img/dashboard_links.png)
When you click **Issues**, you'll see the opened issues assigned to you straight away:
![Issues assigned to you](img/issues_assigned_to_you.png)
-You can filter them by **Author**, **Assignee**, **Milestone**, and **Labels**,
-searching through **Open**, **Closed**, and **All** issues.
+You can search through **Open**, **Closed**, or **All** issues.
-Of course, you can combine all filters together.
+You can also filter the results using the search and filter field. This works in the same way as the ones found in the
+per project pages described below.
### Issues and MRs assigned to you or created by you
-You'll find a shortcut to issues and merge requests create by you or assigned to you
+You'll also find shortcuts to issues and merge requests created by you or assigned to you
on the search field on the top-right of your screen:
![shortcut to your issues and mrs](img/issues_mrs_shortcut.png)
@@ -40,6 +40,16 @@ The same process is valid for merge requests. Navigate to your project's **Merge
and click **Search or filter results...**. Merge requests can be filtered by author, assignee,
milestone, and label.
+### Filtering by **None** / **Any**
+
+Some filter fields like milestone and assignee, allow you to filter by **None** or **Any**.
+
+![filter by none any](img/issues_filter_none_any.png)
+
+Selecting **None** returns results that have an empty value for that field. E.g.: no milestone, no assignee.
+
+Selecting **Any** does the opposite. It returns results that have a non-empty value for that field.
+
### Searching for specific terms
You can filter issues and merge requests by specific terms included in titles or descriptions.
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..337b92a6183 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.5'
+ 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 18c30723d73..5572e86985c 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -160,13 +160,27 @@ module API
# (fixed in https://github.com/rails/rails/pull/25976).
project.tags.map(&:name).sort
end
+
expose :ssh_url_to_repo, :http_url_to_repo, :web_url, :readme_url
+
+ expose :license_url, if: :license do |project|
+ license = project.repository.license_blob
+
+ if license
+ Gitlab::Routing.url_helpers.project_blob_url(project, File.join(project.default_branch, license.path))
+ end
+ end
+
+ expose :license, with: 'API::Entities::LicenseBasic', if: :license do |project|
+ project.repository.license
+ end
+
expose :avatar_url do |project, options|
project.avatar_url(only_path: false)
end
+
expose :star_count, :forks_count
expose :last_activity_at
-
expose :namespace, using: 'API::Entities::NamespaceBasic'
expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes
@@ -593,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
@@ -662,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
@@ -1208,11 +1222,14 @@ module API
expose :deployable, using: Entities::Job
end
- class License < Grape::Entity
+ class LicenseBasic < Grape::Entity
expose :key, :name, :nickname
- expose :popular?, as: :popular
expose :url, as: :html_url
expose(:source_url) { |license| license.meta['source'] }
+ end
+
+ class License < LicenseBasic
+ expose :popular?, as: :popular
expose(:description) { |license| license.meta['description'] }
expose(:conditions) { |license| license.meta['conditions'] }
expose(:permissions) { |license| license.meta['permissions'] }
@@ -1246,7 +1263,11 @@ module API
expose :token
end
- class ImpersonationToken < PersonalAccessTokenWithToken
+ class ImpersonationToken < PersonalAccessToken
+ expose :impersonation
+ end
+
+ class ImpersonationTokenWithToken < PersonalAccessTokenWithToken
expose :impersonation
end
diff --git a/lib/api/helpers/custom_validators.rb b/lib/api/helpers/custom_validators.rb
index 23b1cd1ad45..1058f4e8a5e 100644
--- a/lib/api/helpers/custom_validators.rb
+++ b/lib/api/helpers/custom_validators.rb
@@ -10,8 +10,21 @@ module API
raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:absence)
end
end
+
+ class IntegerNoneAny < Grape::Validations::Base
+ def validate_param!(attr_name, params)
+ value = params[attr_name]
+
+ return if value.is_a?(Integer) ||
+ [IssuableFinder::FILTER_NONE, IssuableFinder::FILTER_ANY].include?(value.to_s.downcase)
+
+ raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)],
+ message: "should be an integer, 'None' or 'Any'"
+ end
+ end
end
end
end
Grape::Validations.register_validator(:absence, ::API::Helpers::CustomValidators::Absence)
+Grape::Validations.register_validator(:integer_none_any, ::API::Helpers::CustomValidators::IntegerNoneAny)
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 4dd6b19e353..ae40b5f7557 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -65,6 +65,8 @@ module API
result
rescue Gitlab::GitAccess::UnauthorizedError => e
break response_with_status(code: 401, success: false, message: e.message)
+ rescue Gitlab::GitAccess::TimeoutError => e
+ break response_with_status(code: 503, success: false, message: e.message)
rescue Gitlab::GitAccess::NotFoundError => e
break response_with_status(code: 404, success: false, message: e.message)
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 25d78053c88..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
@@ -40,14 +49,17 @@ module API
optional :updated_after, type: DateTime, desc: 'Return issues updated after the specified time'
optional :updated_before, type: DateTime, desc: 'Return issues updated before the specified time'
optional :author_id, type: Integer, desc: 'Return issues which are authored by the user with the given ID'
- optional :assignee_id, type: Integer, desc: 'Return issues which are assigned to the user with the given ID'
+ optional :assignee_id, types: [Integer, String], integer_none_any: true,
+ desc: 'Return issues which are assigned to the user with the given ID'
optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all],
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'
@@ -56,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
@@ -284,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 440d94ae186..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
@@ -89,7 +89,8 @@ module API
optional :updated_before, type: DateTime, desc: 'Return merge requests updated before the specified time'
optional :view, type: String, values: %w[simple], desc: 'If simple, returns the `iid`, URL, title, description, and basic state of merge request'
optional :author_id, type: Integer, desc: 'Return merge requests which are authored by the user with the given ID'
- optional :assignee_id, type: Integer, desc: 'Return merge requests which are assigned to the user with the given ID'
+ optional :assignee_id, types: [Integer, String], integer_none_any: true,
+ desc: 'Return merge requests which are assigned to the user with the given ID'
optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all],
desc: 'Return merge requests 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'
diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb
index 1cfb982c04b..cba1e3a6684 100644
--- a/lib/api/pipelines.rb
+++ b/lib/api/pipelines.rb
@@ -81,6 +81,21 @@ module API
present pipeline, with: Entities::Pipeline
end
+ desc 'Deletes a pipeline' do
+ detail 'This feature was introduced in GitLab 11.6'
+ http_codes [[204, 'Pipeline was deleted'], [403, 'Forbidden']]
+ end
+ params do
+ requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
+ end
+ delete ':id/pipelines/:pipeline_id' do
+ authorize! :destroy_pipeline, pipeline
+
+ destroy_conditionally!(pipeline) do
+ ::Ci::DestroyPipelineService.new(user_project, current_user).execute(pipeline)
+ end
+ end
+
desc 'Retry builds in the pipeline' do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::Pipeline
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index ae2d327e45b..0a914f9012e 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -114,7 +114,8 @@ module API
options = options.reverse_merge(
with: current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails,
statistics: params[:statistics],
- current_user: current_user
+ current_user: current_user,
+ license: false
)
options[:with] = Entities::BasicProjectDetails if params[:simple]
@@ -230,13 +231,17 @@ module API
params do
use :statistics_params
use :with_custom_attributes
+
+ optional :license, type: Boolean, default: false,
+ desc: 'Include project license data'
end
get ":id" do
options = {
with: current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails,
current_user: current_user,
user_can_admin_project: can?(current_user, :admin_project, user_project),
- statistics: params[:statistics]
+ statistics: params[:statistics],
+ license: params[:license]
}
project, options = with_custom_attributes(user_project, options)
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index d8768a54986..2f15f3a7d76 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -142,8 +142,7 @@ module API
requires :id, type: Integer, desc: %q(Job's ID)
optional :trace, type: String, desc: %q(Job's full trace)
optional :state, type: String, desc: %q(Job's status: success, failed)
- optional :failure_reason, type: String, values: CommitStatus.failure_reasons.keys,
- desc: %q(Job's failure_reason)
+ optional :failure_reason, type: String, desc: %q(Job's failure_reason)
end
put '/:id' do
job = authenticate_job!
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..b41fce76df0 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! }
@@ -533,7 +531,7 @@ module API
desc 'Create a impersonation token. Available only for admins.' do
detail 'This feature was introduced in GitLab 9.0'
- success Entities::ImpersonationToken
+ success Entities::ImpersonationTokenWithToken
end
params do
requires :name, type: String, desc: 'The name of the impersonation token'
@@ -544,7 +542,7 @@ module API
impersonation_token = finder.build(declared_params(include_missing: false))
if impersonation_token.save
- present impersonation_token, with: Entities::ImpersonationToken
+ present impersonation_token, with: Entities::ImpersonationTokenWithToken
else
render_validation_error!(impersonation_token)
end
diff --git a/lib/api/validations/types/safe_file.rb b/lib/api/validations/types/safe_file.rb
new file mode 100644
index 00000000000..53b5790bfa2
--- /dev/null
+++ b/lib/api/validations/types/safe_file.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+# This module overrides the Grape type validator defined in
+# https://github.com/ruby-grape/grape/blob/master/lib/grape/validations/types/file.rb
+module API
+ module Validations
+ module Types
+ class SafeFile < ::Grape::Validations::Types::File
+ def value_coerced?(value)
+ super && value[:tempfile].is_a?(Tempfile)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb
index 6e1d4eb335f..24746f4efc6 100644
--- a/lib/api/wikis.rb
+++ b/lib/api/wikis.rb
@@ -6,7 +6,7 @@ module API
def commit_params(attrs)
{
file_name: attrs[:file][:filename],
- file_content: File.read(attrs[:file][:tempfile]),
+ file_content: attrs[:file][:tempfile].read,
branch_name: attrs[:branch]
}
end
@@ -100,7 +100,7 @@ module API
success Entities::WikiAttachment
end
params do
- requires :file, type: File, desc: 'The attachment file to be uploaded'
+ requires :file, type: ::API::Validations::Types::SafeFile, desc: 'The attachment file to be uploaded'
optional :branch, type: String, desc: 'The name of the branch'
end
post ":id/wikis/attachments", requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
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.rb b/lib/gitlab.rb
index 2bb09684441..2ef54658a11 100644
--- a/lib/gitlab.rb
+++ b/lib/gitlab.rb
@@ -53,4 +53,8 @@ module Gitlab
def self.pre_release?
VERSION.include?('pre')
end
+
+ def self.version_info
+ Gitlab::VersionInfo.parse(Gitlab::VERSION)
+ end
end
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index d2029a141e7..6eb5f9e2300 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -151,17 +151,15 @@ module Gitlab
end
# rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
def personal_access_token_check(password)
return unless password.present?
- token = PersonalAccessTokensFinder.new(state: 'active').find_by(token: password)
+ token = PersonalAccessTokensFinder.new(state: 'active').find_by_token(password)
if token && valid_scoped_token?(token, available_scopes)
Gitlab::Auth::Result.new(token.user, nil, :personal_access_token, abilities_for_scopes(token.scopes))
end
end
- # rubocop: enable CodeReuse/ActiveRecord
def valid_oauth_token?(token)
token && token.accessible? && valid_scoped_token?(token, [:api])
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/auth/user_auth_finders.rb b/lib/gitlab/auth/user_auth_finders.rb
index 5df6db6f366..c304adc64db 100644
--- a/lib/gitlab/auth/user_auth_finders.rb
+++ b/lib/gitlab/auth/user_auth_finders.rb
@@ -73,7 +73,6 @@ module Gitlab
end
end
- # rubocop: disable CodeReuse/ActiveRecord
def find_personal_access_token
token =
current_request.params[PRIVATE_TOKEN_PARAM].presence ||
@@ -82,9 +81,8 @@ module Gitlab
return unless token
# Expiration, revocation and scopes are verified in `validate_access_token!`
- PersonalAccessToken.find_by(token: token) || raise(UnauthorizedError)
+ PersonalAccessToken.find_by_token(token) || raise(UnauthorizedError)
end
- # rubocop: enable CodeReuse/ActiveRecord
def find_oauth_access_token
token = Doorkeeper::OAuth::Token.from_request(current_request, *Doorkeeper.configuration.access_token_methods)
diff --git a/lib/gitlab/background_migration/digest_column.rb b/lib/gitlab/background_migration/digest_column.rb
new file mode 100644
index 00000000000..22a3bb8f8f3
--- /dev/null
+++ b/lib/gitlab/background_migration/digest_column.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+# rubocop:disable Style/Documentation
+module Gitlab
+ module BackgroundMigration
+ class DigestColumn
+ class PersonalAccessToken < ActiveRecord::Base
+ self.table_name = 'personal_access_tokens'
+ end
+
+ def perform(model, attribute_from, attribute_to, start_id, stop_id)
+ model = model.constantize if model.is_a?(String)
+
+ model.transaction do
+ relation = model.where(id: start_id..stop_id).where.not(attribute_from => nil).lock
+
+ relation.each do |instance|
+ instance.update_columns(attribute_to => Gitlab::CryptoHelper.sha256(instance.read_attribute(attribute_from)),
+ attribute_from => nil)
+ end
+ end
+ end
+ end
+ end
+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
new file mode 100644
index 00000000000..92256e59a6c
--- /dev/null
+++ b/lib/gitlab/background_migration/redact_links.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+require_relative 'redact_links/redactable'
+
+module Gitlab
+ module BackgroundMigration
+ class RedactLinks
+ class Note < ActiveRecord::Base
+ include EachBatch
+ include ::Gitlab::BackgroundMigration::RedactLinks::Redactable
+
+ self.table_name = 'notes'
+ self.inheritance_column = :_type_disabled
+ end
+
+ class Issue < ActiveRecord::Base
+ include EachBatch
+ include ::Gitlab::BackgroundMigration::RedactLinks::Redactable
+
+ self.table_name = 'issues'
+ self.inheritance_column = :_type_disabled
+ end
+
+ class MergeRequest < ActiveRecord::Base
+ include EachBatch
+ include ::Gitlab::BackgroundMigration::RedactLinks::Redactable
+
+ self.table_name = 'merge_requests'
+ self.inheritance_column = :_type_disabled
+ end
+
+ class Snippet < ActiveRecord::Base
+ include EachBatch
+ include ::Gitlab::BackgroundMigration::RedactLinks::Redactable
+
+ self.table_name = 'snippets'
+ self.inheritance_column = :_type_disabled
+ end
+
+ def perform(model_name, field, start_id, stop_id)
+ link_pattern = "%/sent_notifications/" + ("_" * 32) + "/unsubscribe%"
+ model = "Gitlab::BackgroundMigration::RedactLinks::#{model_name}".constantize
+
+ model.where("#{field} like ?", link_pattern).where(id: start_id..stop_id).each do |resource|
+ resource.redact_field!(field)
+ end
+ end
+ end
+ end
+end
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/cache/ci/project_pipeline_status.rb b/lib/gitlab/cache/ci/project_pipeline_status.rb
index b369b9e7600..78b0eaac8cd 100644
--- a/lib/gitlab/cache/ci/project_pipeline_status.rb
+++ b/lib/gitlab/cache/ci/project_pipeline_status.rb
@@ -42,7 +42,7 @@ module Gitlab
end
def self.cache_key_for_project(project)
- "#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:projects/#{project.id}/pipeline_status"
+ "#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:project:#{project.id}:pipeline_status:#{project.commit&.sha}"
end
def self.update_for_pipeline(pipeline)
@@ -84,9 +84,7 @@ module Gitlab
def load_from_project
return unless commit
- self.sha = commit.sha
- self.status = commit.status
- self.ref = project.default_branch
+ self.sha, self.status, self.ref = commit.sha, commit.status, project.default_branch
end
# We only cache the status for the HEAD commit of a project
@@ -104,6 +102,8 @@ module Gitlab
def load_from_cache
Gitlab::Redis::Cache.with do |redis|
self.sha, self.status, self.ref = redis.hmget(cache_key, :sha, :status, :ref)
+
+ self.status = nil if self.status.empty?
end
end
diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb
index 49e7f7e1fd7..074afe9c412 100644
--- a/lib/gitlab/checks/change_access.rb
+++ b/lib/gitlab/checks/change_access.rb
@@ -18,11 +18,24 @@ module Gitlab
lfs_objects_missing: 'LFS objects are missing. Ensure LFS is properly set up or try a manual "git lfs push --all".'
}.freeze
- attr_reader :user_access, :project, :skip_authorization, :skip_lfs_integrity_check, :protocol, :oldrev, :newrev, :ref, :branch_name, :tag_name
+ LOG_MESSAGES = {
+ push_checks: "Checking if you are allowed to push...",
+ delete_default_branch_check: "Checking if default branch is being deleted...",
+ protected_branch_checks: "Checking if you are force pushing to a protected branch...",
+ protected_branch_push_checks: "Checking if you are allowed to push to the protected branch...",
+ protected_branch_deletion_checks: "Checking if you are allowed to delete the protected branch...",
+ tag_checks: "Checking if you are allowed to change existing tags...",
+ protected_tag_checks: "Checking if you are creating, updating or deleting a protected tag...",
+ lfs_objects_exist_check: "Scanning repository for blobs stored in LFS and verifying their files have been uploaded to GitLab...",
+ commits_check_file_paths_validation: "Validating commits' file paths...",
+ commits_check: "Validating commit contents..."
+ }.freeze
+
+ attr_reader :user_access, :project, :skip_authorization, :skip_lfs_integrity_check, :protocol, :oldrev, :newrev, :ref, :branch_name, :tag_name, :logger
def initialize(
change, user_access:, project:, skip_authorization: false,
- skip_lfs_integrity_check: false, protocol:
+ skip_lfs_integrity_check: false, protocol:, logger:
)
@oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
@branch_name = Gitlab::Git.branch_name(@ref)
@@ -32,6 +45,9 @@ module Gitlab
@skip_authorization = skip_authorization
@skip_lfs_integrity_check = skip_lfs_integrity_check
@protocol = protocol
+
+ @logger = logger
+ @logger.append_message("Running checks for ref: #{@branch_name || @tag_name}")
end
def exec(skip_commits_check: false)
@@ -49,26 +65,32 @@ module Gitlab
protected
def push_checks
- unless can_push?
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:push_code]
+ logger.log_timed(LOG_MESSAGES[__method__]) do
+ unless can_push?
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:push_code]
+ end
end
end
def branch_checks
return unless branch_name
- if deletion? && branch_name == project.default_branch
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_default_branch]
+ logger.log_timed(LOG_MESSAGES[:delete_default_branch_check]) do
+ if deletion? && branch_name == project.default_branch
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_default_branch]
+ end
end
protected_branch_checks
end
def protected_branch_checks
- return unless ProtectedBranch.protected?(project, branch_name)
+ logger.log_timed(LOG_MESSAGES[__method__]) do
+ return unless ProtectedBranch.protected?(project, branch_name) # rubocop:disable Cop/AvoidReturnFromBlocks
- if forced_push?
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:force_push_protected_branch]
+ if forced_push?
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:force_push_protected_branch]
+ end
end
if deletion?
@@ -79,23 +101,27 @@ module Gitlab
end
def protected_branch_deletion_checks
- unless user_access.can_delete_branch?(branch_name)
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_master_delete_protected_branch]
- end
+ logger.log_timed(LOG_MESSAGES[__method__]) do
+ unless user_access.can_delete_branch?(branch_name)
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_master_delete_protected_branch]
+ end
- unless updated_from_web?
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_web_delete_protected_branch]
+ unless updated_from_web?
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_web_delete_protected_branch]
+ end
end
end
def protected_branch_push_checks
- if matching_merge_request?
- unless user_access.can_merge_to_branch?(branch_name) || user_access.can_push_to_branch?(branch_name)
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:merge_protected_branch]
- end
- else
- unless user_access.can_push_to_branch?(branch_name)
- raise GitAccess::UnauthorizedError, push_to_protected_branch_rejected_message
+ logger.log_timed(LOG_MESSAGES[__method__]) do
+ if matching_merge_request?
+ unless user_access.can_merge_to_branch?(branch_name) || user_access.can_push_to_branch?(branch_name)
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:merge_protected_branch]
+ end
+ else
+ unless user_access.can_push_to_branch?(branch_name)
+ raise GitAccess::UnauthorizedError, push_to_protected_branch_rejected_message
+ end
end
end
end
@@ -103,21 +129,25 @@ module Gitlab
def tag_checks
return unless tag_name
- if tag_exists? && user_access.cannot_do_action?(:admin_project)
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:change_existing_tags]
+ logger.log_timed(LOG_MESSAGES[__method__]) do
+ if tag_exists? && user_access.cannot_do_action?(:admin_project)
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:change_existing_tags]
+ end
end
protected_tag_checks
end
def protected_tag_checks
- return unless ProtectedTag.protected?(project, tag_name)
+ logger.log_timed(LOG_MESSAGES[__method__]) do
+ return unless ProtectedTag.protected?(project, tag_name) # rubocop:disable Cop/AvoidReturnFromBlocks
- raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:update_protected_tag]) if update?
- raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_protected_tag]) if deletion?
+ raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:update_protected_tag]) if update?
+ raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_protected_tag]) if deletion?
- unless user_access.can_create_tag?(tag_name)
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:create_protected_tag]
+ unless user_access.can_create_tag?(tag_name)
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:create_protected_tag]
+ end
end
end
@@ -125,14 +155,20 @@ module Gitlab
return if deletion? || newrev.nil?
return unless should_run_commit_validations?
- # n+1: https://gitlab.com/gitlab-org/gitlab-ee/issues/3593
- ::Gitlab::GitalyClient.allow_n_plus_1_calls do
- commits.each do |commit|
- commit_check.validate(commit, validations_for_commit(commit))
+ logger.log_timed(LOG_MESSAGES[__method__]) do
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ee/issues/3593
+ ::Gitlab::GitalyClient.allow_n_plus_1_calls do
+ commits.each do |commit|
+ logger.check_timeout_reached
+
+ commit_check.validate(commit, validations_for_commit(commit))
+ end
end
end
- commit_check.validate_file_paths
+ logger.log_timed(LOG_MESSAGES[:commits_check_file_paths_validation]) do
+ commit_check.validate_file_paths
+ end
end
# Method overwritten in EE to inject custom validations
@@ -194,10 +230,12 @@ module Gitlab
end
def lfs_objects_exist_check
- lfs_check = Checks::LfsIntegrity.new(project, newrev)
+ logger.log_timed(LOG_MESSAGES[__method__]) do
+ lfs_check = Checks::LfsIntegrity.new(project, newrev, logger.time_left)
- if lfs_check.objects_missing?
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:lfs_objects_missing]
+ if lfs_check.objects_missing?
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:lfs_objects_missing]
+ end
end
end
diff --git a/lib/gitlab/checks/commit_check.rb b/lib/gitlab/checks/commit_check.rb
index 6dd74e8fb74..58267b6752f 100644
--- a/lib/gitlab/checks/commit_check.rb
+++ b/lib/gitlab/checks/commit_check.rb
@@ -10,8 +10,8 @@ module Gitlab
def initialize(project, user, newrev, oldrev)
@project = project
@user = user
- @newrev = user
- @oldrev = user
+ @newrev = newrev
+ @oldrev = oldrev
@file_paths = []
end
diff --git a/lib/gitlab/checks/lfs_integrity.rb b/lib/gitlab/checks/lfs_integrity.rb
index fa3dc1808df..1652d5a30a4 100644
--- a/lib/gitlab/checks/lfs_integrity.rb
+++ b/lib/gitlab/checks/lfs_integrity.rb
@@ -3,9 +3,10 @@
module Gitlab
module Checks
class LfsIntegrity
- def initialize(project, newrev)
+ def initialize(project, newrev, time_left)
@project = project
@newrev = newrev
+ @time_left = time_left
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -13,7 +14,7 @@ module Gitlab
return false unless @newrev && @project.lfs_enabled?
new_lfs_pointers = Gitlab::Git::LfsChanges.new(@project.repository, @newrev)
- .new_pointers(object_limit: ::Gitlab::Git::Repository::REV_LIST_COMMIT_LIMIT)
+ .new_pointers(object_limit: ::Gitlab::Git::Repository::REV_LIST_COMMIT_LIMIT, dynamic_timeout: @time_left)
return false unless new_lfs_pointers.present?
diff --git a/lib/gitlab/checks/timed_logger.rb b/lib/gitlab/checks/timed_logger.rb
new file mode 100644
index 00000000000..f365e0a43f6
--- /dev/null
+++ b/lib/gitlab/checks/timed_logger.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Checks
+ class TimedLogger
+ TimeoutError = Class.new(StandardError)
+
+ attr_reader :start_time, :header, :log, :timeout
+
+ def initialize(start_time: Time.now, log: [], header: "", timeout:)
+ @start_time = start_time
+ @timeout = timeout
+ @header = header
+ @log = log
+ end
+
+ # Adds trace of method being tracked with
+ # the correspondent time it took to run it.
+ # We make use of the start default argument
+ # on unit tests related to this method
+ #
+ def log_timed(log_message, start = Time.now)
+ check_timeout_reached
+
+ timed = true
+
+ yield
+
+ append_message(log_message + time_suffix_message(start: start))
+ rescue GRPC::DeadlineExceeded, TimeoutError
+ args = { cancelled: true }
+ args[:start] = start if timed
+
+ append_message(log_message + time_suffix_message(args))
+
+ raise TimeoutError
+ end
+
+ def check_timeout_reached
+ return unless time_expired?
+
+ raise TimeoutError
+ end
+
+ def time_left
+ (start_time + timeout.seconds) - Time.now
+ end
+
+ def full_message
+ header + log.join("\n")
+ end
+
+ # We always want to append in-place on the log
+ def append_message(message)
+ log << message
+ end
+
+ private
+
+ def time_expired?
+ time_left <= 0
+ end
+
+ def time_suffix_message(cancelled: false, start: nil)
+ return " (#{elapsed_time(start)}ms)" unless cancelled
+
+ if start
+ " (cancelled after #{elapsed_time(start)}ms)"
+ else
+ " (cancelled)"
+ end
+ end
+
+ def elapsed_time(start)
+ to_ms(Time.now - start)
+ end
+
+ def to_ms(elapsed)
+ (elapsed.to_f * 1000).round(2)
+ end
+ end
+ end
+end
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/changes.rb b/lib/gitlab/ci/build/policy/changes.rb
index 7bf51519752..1663c875426 100644
--- a/lib/gitlab/ci/build/policy/changes.rb
+++ b/lib/gitlab/ci/build/policy/changes.rb
@@ -14,7 +14,7 @@ module Gitlab
pipeline.modified_paths.any? do |path|
@globs.any? do |glob|
- File.fnmatch?(glob, path, File::FNM_PATHNAME | File::FNM_DOTMATCH)
+ File.fnmatch?(glob, path, File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB)
end
end
end
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/parsers/test/junit.rb b/lib/gitlab/ci/parsers/test/junit.rb
index 5d7d9a751d8..2791730fd26 100644
--- a/lib/gitlab/ci/parsers/test/junit.rb
+++ b/lib/gitlab/ci/parsers/test/junit.rb
@@ -14,10 +14,10 @@ module Gitlab
test_case = create_test_case(test_case)
test_suite.add_test_case(test_case)
end
- rescue REXML::ParseException => e
- raise JunitParserError, "XML parsing failed: #{e.message}"
- rescue => e
- raise JunitParserError, "JUnit parsing failed: #{e.message}"
+ rescue Nokogiri::XML::SyntaxError
+ raise JunitParserError, "XML parsing failed"
+ rescue
+ raise JunitParserError, "JUnit parsing failed"
end
private
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 50b0d044265..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
@@ -11,7 +13,9 @@ module Gitlab
runner_system_failure: 'runner system failure',
missing_dependency_failure: 'missing dependency failure',
runner_unsupported: 'unsupported runner',
- stale_schedule: 'stale schedule'
+ stale_schedule: 'stale schedule',
+ 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 6fa59e41d20..c90976b2040 100644
--- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
@@ -19,6 +19,15 @@
# * review: REVIEW_DISABLED
# * stop_review: REVIEW_DISABLED
#
+# The sast and sast_dashboard jobs are executed to guarantee full compatibility
+# with the group security dashboard and the security reports with old runners.
+# If you use only runners with version 11.5 or above, you can disable the sast
+# job by setting the OLD_REPORTS_DISABLED environment variable. If you use only
+# runners with version below 11.5, you can disable the sast_dashboard job by
+# setting the NEW_REPORTS_DISABLED environment variable.
+# The sast_dashboard job will be removed in the future, when the sast job will
+# use the new reports syntax.
+#
# In order to deploy, you must have a Kubernetes cluster configured either
# via a project integration, or via group/project variables.
# AUTO_DEVOPS_DOMAIN must also be set as a variable at the group or project
@@ -27,7 +36,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 +58,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 +125,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 +158,10 @@ performance:
only:
refs:
- branches
- kubernetes: active
except:
variables:
- $PERFORMANCE_DISABLED
+ - $KUBECONFIG == null
sast:
stage: test
@@ -174,6 +182,29 @@ sast:
except:
variables:
- $SAST_DISABLED
+ - $OLD_REPORTS_DISABLED
+
+sast_dashboard:
+ stage: test
+ image: docker:stable
+ allow_failure: true
+ services:
+ - docker:stable-dind
+ script:
+ - setup_docker
+ - sast
+ artifacts:
+ reports:
+ sast: gl-sast-report.json
+ only:
+ refs:
+ - branches
+ variables:
+ - $GITLAB_FEATURES =~ /\bsast\b/
+ except:
+ variables:
+ - $SAST_DISABLED
+ - $NEW_REPORTS_DISABLED
dependency_scanning:
stage: test
@@ -210,7 +241,7 @@ container_scanning:
refs:
- branches
variables:
- - $GITLAB_FEATURES =~ /\bsast_container\b/
+ - $GITLAB_FEATURES =~ /\bcontainer_scanning\b/
except:
variables:
- $CONTAINER_SCANNING_DISABLED
@@ -228,7 +259,6 @@ dast:
only:
refs:
- branches
- kubernetes: active
variables:
- $GITLAB_FEATURES =~ /\bdast\b/
except:
@@ -236,6 +266,7 @@ dast:
- master
variables:
- $DAST_DISABLED
+ - $KUBECONFIG == null
review:
stage: review
@@ -257,12 +288,12 @@ review:
only:
refs:
- branches
- kubernetes: active
except:
refs:
- master
variables:
- $REVIEW_DISABLED
+ - $KUBECONFIG == null
stop_review:
stage: cleanup
@@ -280,12 +311,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 +340,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 +367,11 @@ canary:
only:
refs:
- master
- kubernetes: active
variables:
- $CANARY_ENABLED
+ except:
+ variables:
+ - $KUBECONFIG == null
.production: &production_template
stage: production
@@ -362,13 +397,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 +412,6 @@ production_manual:
only:
refs:
- master
- kubernetes: active
variables:
- $STAGING_ENABLED
- $CANARY_ENABLED
@@ -385,6 +419,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 +449,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 +464,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 +562,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 +856,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..cf8958e34c2 100644
--- a/lib/gitlab/ci/variables/collection/item.rb
+++ b/lib/gitlab/ci/variables/collection/item.rb
@@ -1,11 +1,13 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Variables
class Collection
class Item
def initialize(key:, value:, public: true, file: false)
- raise ArgumentError, "`value` must be of type String, while it was: #{value.class}" unless
- value.is_a?(String) || value.nil?
+ raise ArgumentError, "`#{key}` must be of type String, while it was: #{value.class}" unless
+ value.is_a?(String)
@variable = {
key: key, value: value, public: public, file: file
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/lifecycle_events.rb b/lib/gitlab/cluster/lifecycle_events.rb
new file mode 100644
index 00000000000..b05dca409d1
--- /dev/null
+++ b/lib/gitlab/cluster/lifecycle_events.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Cluster
+ #
+ # LifecycleEvents lets Rails initializers register application startup hooks
+ # that are sensitive to forking. For example, to defer the creation of
+ # watchdog threads. This lets us abstract away the Unix process
+ # lifecycles of Unicorn, Sidekiq, Puma, Puma Cluster, etc.
+ #
+ # We have three lifecycle events.
+ #
+ # - before_fork (only in forking processes)
+ # - worker_start
+ # - before_master_restart (only in forking processes)
+ #
+ # Blocks will be executed in the order in which they are registered.
+ #
+ class LifecycleEvents
+ class << self
+ #
+ # Hook registration methods (called from initializers)
+ #
+ def on_worker_start(&block)
+ if in_clustered_environment?
+ # Defer block execution
+ (@worker_start_hooks ||= []) << block
+ else
+ yield
+ end
+ end
+
+ def on_before_fork(&block)
+ return unless in_clustered_environment?
+
+ # Defer block execution
+ (@before_fork_hooks ||= []) << block
+ end
+
+ def on_master_restart(&block)
+ return unless in_clustered_environment?
+
+ # Defer block execution
+ (@master_restart_hooks ||= []) << block
+ end
+
+ #
+ # Lifecycle integration methods (called from unicorn.rb, puma.rb, etc.)
+ #
+ def do_worker_start
+ @worker_start_hooks&.each do |block|
+ block.call
+ end
+ end
+
+ def do_before_fork
+ @before_fork_hooks&.each do |block|
+ block.call
+ end
+ end
+
+ def do_master_restart
+ @master_restart_hooks && @master_restart_hooks.each do |block|
+ block.call
+ end
+ end
+
+ # Puma doesn't use singletons (which is good) but
+ # this means we need to pass through whether the
+ # puma server is running in single mode or cluster mode
+ def set_puma_options(options)
+ @puma_options = options
+ end
+
+ private
+
+ def in_clustered_environment?
+ # Sidekiq doesn't fork
+ return false if Sidekiq.server?
+
+ # Unicorn always forks
+ return true if defined?(::Unicorn)
+
+ # Puma sometimes forks
+ return true if in_clustered_puma?
+
+ # Default assumption is that we don't fork
+ false
+ end
+
+ def in_clustered_puma?
+ return false unless defined?(::Puma)
+
+ @puma_options && @puma_options[:workers] && @puma_options[:workers] > 0
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cluster/puma_worker_killer_initializer.rb b/lib/gitlab/cluster/puma_worker_killer_initializer.rb
new file mode 100644
index 00000000000..4ed9a9a02ab
--- /dev/null
+++ b/lib/gitlab/cluster/puma_worker_killer_initializer.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Cluster
+ class PumaWorkerKillerInitializer
+ def self.start(puma_options, puma_per_worker_max_memory_mb: 650)
+ require 'puma_worker_killer'
+
+ PumaWorkerKiller.config do |config|
+ # Note! ram is expressed in megabytes (whereas GITLAB_UNICORN_MEMORY_MAX is in bytes)
+ # 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
+ # 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
+
+ # We just want to limit to a fixed maximum, unrelated to the total amount
+ # of available RAM.
+ config.percent_usage = 0.98
+
+ # Ideally we'll never hit the maximum amount of memory. If so the worker
+ # is restarted already, thus periodically restarting workers shouldn't be
+ # needed.
+ config.rolling_restart_frequency = false
+ end
+
+ PumaWorkerKiller.start
+ end
+ end
+ end
+end
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/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb
index c819bffdfd6..5ed6427072a 100644
--- a/lib/gitlab/contributions_calendar.rb
+++ b/lib/gitlab/contributions_calendar.rb
@@ -73,7 +73,7 @@ module Gitlab
# re-running the contributed projects query in each union is expensive, so
# use IN(project_ids...) instead. It's the intersection of two users so
# the list will be (relatively) short
- @contributed_project_ids ||= projects.uniq.pluck(:id)
+ @contributed_project_ids ||= projects.distinct.pluck(:id)
authed_projects = Project.where(id: @contributed_project_ids)
.with_feature_available_for_user(feature, current_user)
.reorder(nil)
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/crypto_helper.rb b/lib/gitlab/crypto_helper.rb
new file mode 100644
index 00000000000..68d0b5d8f8a
--- /dev/null
+++ b/lib/gitlab/crypto_helper.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module CryptoHelper
+ extend self
+
+ AES256_GCM_OPTIONS = {
+ algorithm: 'aes-256-gcm',
+ key: Settings.attr_encrypted_db_key_base_truncated,
+ iv: Settings.attr_encrypted_db_key_base_truncated[0..11]
+ }.freeze
+
+ def sha256(value)
+ salt = Settings.attr_encrypted_db_key_base_truncated
+ ::Digest::SHA256.base64digest("#{value}#{salt}")
+ end
+
+ def aes256_gcm_encrypt(value)
+ encrypted_token = Encryptor.encrypt(AES256_GCM_OPTIONS.merge(value: value))
+ Base64.encode64(encrypted_token)
+ end
+
+ def aes256_gcm_decrypt(value)
+ return unless value
+
+ encrypted_token = Base64.decode64(value)
+ Encryptor.decrypt(AES256_GCM_OPTIONS.merge(value: encrypted_token))
+ end
+ end
+end
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..2770469ca9f 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: /\A(#{Regexp.union(*Gitlab::MarkupHelper::PLAIN_FILENAMES).source})(\.(#{Regexp.union(*Gitlab::MarkupHelper::EXTENSIONS).source}))?\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/fogbugz_import/client.rb b/lib/gitlab/fogbugz_import/client.rb
index acb000e3e23..dd747a79673 100644
--- a/lib/gitlab/fogbugz_import/client.rb
+++ b/lib/gitlab/fogbugz_import/client.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'fogbugz'
module Gitlab
diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb
index 98ea5b309a1..431911d1eee 100644
--- a/lib/gitlab/fogbugz_import/importer.rb
+++ b/lib/gitlab/fogbugz_import/importer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module FogbugzImport
class Importer
diff --git a/lib/gitlab/fogbugz_import/project_creator.rb b/lib/gitlab/fogbugz_import/project_creator.rb
index 1918d5b208d..3c71031a8d9 100644
--- a/lib/gitlab/fogbugz_import/project_creator.rb
+++ b/lib/gitlab/fogbugz_import/project_creator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module FogbugzImport
class ProjectCreator
diff --git a/lib/gitlab/fogbugz_import/repository.rb b/lib/gitlab/fogbugz_import/repository.rb
index d1dc63db2b2..b958dcf6cbf 100644
--- a/lib/gitlab/fogbugz_import/repository.rb
+++ b/lib/gitlab/fogbugz_import/repository.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module FogbugzImport
class Repository
diff --git a/lib/gitlab/gfm/reference_rewriter.rb b/lib/gitlab/gfm/reference_rewriter.rb
index 455814a9159..08d7db49ad7 100644
--- a/lib/gitlab/gfm/reference_rewriter.rb
+++ b/lib/gitlab/gfm/reference_rewriter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Gfm
##
@@ -31,19 +33,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 +55,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 +74,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 +93,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..3f06badf5d9 100644
--- a/lib/gitlab/gfm/uploads_rewriter.rb
+++ b/lib/gitlab/gfm/uploads_rewriter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'fileutils'
module Gitlab
@@ -16,14 +18,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/attributes_at_ref_parser.rb b/lib/gitlab/git/attributes_at_ref_parser.rb
index 26b5bd520d5..cbddf836ce8 100644
--- a/lib/gitlab/git/attributes_at_ref_parser.rb
+++ b/lib/gitlab/git/attributes_at_ref_parser.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Git
# Parses root .gitattributes file at a given ref
diff --git a/lib/gitlab/git/attributes_parser.rb b/lib/gitlab/git/attributes_parser.rb
index 08f4d7d4f5c..8b9d74ae8e7 100644
--- a/lib/gitlab/git/attributes_parser.rb
+++ b/lib/gitlab/git/attributes_parser.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Git
# Class for parsing Git attribute files and extracting the attributes for
diff --git a/lib/gitlab/git/blame.rb b/lib/gitlab/git/blame.rb
index e25e15f5c80..b118eda37f8 100644
--- a/lib/gitlab/git/blame.rb
+++ b/lib/gitlab/git/blame.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Git
class Blame
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index 13b0bb930f4..2d25389594e 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -1,10 +1,11 @@
-# Gitaly note: JV: seems to be completely migrated (behind feature flags).
+# frozen_string_literal: true
module Gitlab
module Git
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/branch.rb b/lib/gitlab/git/branch.rb
index 6351cfb83e3..9447cfa0fb6 100644
--- a/lib/gitlab/git/branch.rb
+++ b/lib/gitlab/git/branch.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Git
class Branch < Ref
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index 74cdabfed9d..4f05c4b73a1 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -1,8 +1,11 @@
+# frozen_string_literal: true
+
# Gitlab::Git::Commit is a wrapper around Gitaly::GitCommit
module Gitlab
module Git
class Commit
include Gitlab::EncodingHelper
+ extend Gitlab::Git::WrapsGitalyErrors
attr_accessor :raw_commit, :head
@@ -59,7 +62,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 +103,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 +128,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 +145,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..8815088d23c 100644
--- a/lib/gitlab/git/commit_stats.rb
+++ b/lib/gitlab/git/commit_stats.rb
@@ -1,8 +1,12 @@
+# frozen_string_literal: true
+
# Gitlab::Git::CommitStats counts the additions, deletions, and total changes
# in a commit.
module Gitlab
module Git
class CommitStats
+ include Gitlab::Git::WrapsGitalyErrors
+
attr_reader :id, :additions, :deletions, :total
# Instantiate a CommitStats object
@@ -14,7 +18,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/compare.rb b/lib/gitlab/git/compare.rb
index 7cb842256d0..ab5245ba7cb 100644
--- a/lib/gitlab/git/compare.rb
+++ b/lib/gitlab/git/compare.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Gitaly note: JV: no RPC's here.
module Gitlab
diff --git a/lib/gitlab/git/conflict/file.rb b/lib/gitlab/git/conflict/file.rb
index f08dab59ce4..7ffe4a7ae81 100644
--- a/lib/gitlab/git/conflict/file.rb
+++ b/lib/gitlab/git/conflict/file.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Git
module Conflict
diff --git a/lib/gitlab/git/conflict/parser.rb b/lib/gitlab/git/conflict/parser.rb
index fb5717dd556..20de8ebde4e 100644
--- a/lib/gitlab/git/conflict/parser.rb
+++ b/lib/gitlab/git/conflict/parser.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Git
module Conflict
diff --git a/lib/gitlab/git/conflict/resolution.rb b/lib/gitlab/git/conflict/resolution.rb
index ab9be683e15..04299a2d10c 100644
--- a/lib/gitlab/git/conflict/resolution.rb
+++ b/lib/gitlab/git/conflict/resolution.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Git
module Conflict
diff --git a/lib/gitlab/git/conflict/resolver.rb b/lib/gitlab/git/conflict/resolver.rb
index 6dc792c16b8..26e82643a4c 100644
--- a/lib/gitlab/git/conflict/resolver.rb
+++ b/lib/gitlab/git/conflict/resolver.rb
@@ -1,7 +1,11 @@
+# frozen_string_literal: true
+
module Gitlab
module Git
module Conflict
class Resolver
+ include Gitlab::Git::WrapsGitalyErrors
+
ConflictSideMissing = Class.new(StandardError)
ResolutionError = Class.new(StandardError)
@@ -12,7 +16,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 +26,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/diff.rb b/lib/gitlab/git/diff.rb
index b2e2d49dd0b..74a4633424f 100644
--- a/lib/gitlab/git/diff.rb
+++ b/lib/gitlab/git/diff.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Git
class Diff
diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb
index 47ebca7c4a2..5c70cb6c66c 100644
--- a/lib/gitlab/git/diff_collection.rb
+++ b/lib/gitlab/git/diff_collection.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Gitaly note: JV: no RPC's here.
module Gitlab
diff --git a/lib/gitlab/git/gitmodules_parser.rb b/lib/gitlab/git/gitmodules_parser.rb
index 4b505312f60..575e12390cd 100644
--- a/lib/gitlab/git/gitmodules_parser.rb
+++ b/lib/gitlab/git/gitmodules_parser.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Gitaly note: JV: no RPC's here.
module Gitlab
diff --git a/lib/gitlab/git/hook_env.rb b/lib/gitlab/git/hook_env.rb
index 620568d8817..892a069a3b7 100644
--- a/lib/gitlab/git/hook_env.rb
+++ b/lib/gitlab/git/hook_env.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Gitaly note: JV: no RPC's here.
module Gitlab
diff --git a/lib/gitlab/git/index.rb b/lib/gitlab/git/index.rb
index c2e4274e3ee..3b9b516308f 100644
--- a/lib/gitlab/git/index.rb
+++ b/lib/gitlab/git/index.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Git
class Index
diff --git a/lib/gitlab/git/lfs_changes.rb b/lib/gitlab/git/lfs_changes.rb
index f0fab1e76a3..8e2a925dfea 100644
--- a/lib/gitlab/git/lfs_changes.rb
+++ b/lib/gitlab/git/lfs_changes.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Git
class LfsChanges
@@ -6,8 +8,8 @@ module Gitlab
@newrev = newrev
end
- def new_pointers(object_limit: nil, not_in: nil)
- @repository.gitaly_blob_client.get_new_lfs_pointers(@newrev, object_limit, not_in)
+ def new_pointers(object_limit: nil, not_in: nil, dynamic_timeout: nil)
+ @repository.gitaly_blob_client.get_new_lfs_pointers(@newrev, object_limit, not_in, dynamic_timeout)
end
def all_pointers
diff --git a/lib/gitlab/git/lfs_pointer_file.rb b/lib/gitlab/git/lfs_pointer_file.rb
index 2ae0a889590..b7019a221ac 100644
--- a/lib/gitlab/git/lfs_pointer_file.rb
+++ b/lib/gitlab/git/lfs_pointer_file.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Git
class LfsPointerFile
diff --git a/lib/gitlab/git/operation_service.rb b/lib/gitlab/git/operation_service.rb
index 0584629ac84..8797d3dce24 100644
--- a/lib/gitlab/git/operation_service.rb
+++ b/lib/gitlab/git/operation_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Git
class OperationService
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/path_helper.rb b/lib/gitlab/git/path_helper.rb
index 57b82a37d6c..e3a2031eeca 100644
--- a/lib/gitlab/git/path_helper.rb
+++ b/lib/gitlab/git/path_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Gitaly note: JV: no RPC's here.
module Gitlab
diff --git a/lib/gitlab/git/pre_receive_error.rb b/lib/gitlab/git/pre_receive_error.rb
index ac1ab7c39d5..03caace6fce 100644
--- a/lib/gitlab/git/pre_receive_error.rb
+++ b/lib/gitlab/git/pre_receive_error.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Git
#
diff --git a/lib/gitlab/git/raw_diff_change.rb b/lib/gitlab/git/raw_diff_change.rb
index 98de9328071..e1002af40f6 100644
--- a/lib/gitlab/git/raw_diff_change.rb
+++ b/lib/gitlab/git/raw_diff_change.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Git
# This class behaves like a struct with fields :blob_id, :blob_size, :operation, :old_path, :new_path
diff --git a/lib/gitlab/git/ref.rb b/lib/gitlab/git/ref.rb
index 31a280155bd..eec91194949 100644
--- a/lib/gitlab/git/ref.rb
+++ b/lib/gitlab/git/ref.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Git
class Ref
diff --git a/lib/gitlab/git/remote_mirror.rb b/lib/gitlab/git/remote_mirror.rb
index e4743b4db0a..e992d522e7f 100644
--- a/lib/gitlab/git/remote_mirror.rb
+++ b/lib/gitlab/git/remote_mirror.rb
@@ -1,13 +1,17 @@
+# frozen_string_literal: true
+
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/remote_repository.rb b/lib/gitlab/git/remote_repository.rb
index f40e59a8dd0..234541d8145 100644
--- a/lib/gitlab/git/remote_repository.rb
+++ b/lib/gitlab/git/remote_repository.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Git
#
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 9df04372cc2..289796ae93b 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'tempfile'
require 'forwardable'
require "rubygems/package"
@@ -6,6 +8,7 @@ module Gitlab
module Git
class Repository
include Gitlab::Git::RepositoryMirroring
+ include Gitlab::Git::WrapsGitalyErrors
include Gitlab::EncodingHelper
include Gitlab::Utils::StrongMemoize
@@ -418,13 +421,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 +577,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 +740,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
@@ -844,26 +885,6 @@ module Gitlab
Gitlab::GitalyClient::ConflictsService.new(self, our_commit_oid, their_commit_oid)
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)
- end
-
def clean_stale_repository_files
wrapped_gitaly_errors do
gitaly_repository_client.cleanup if exists?
@@ -941,6 +962,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/repository_mirroring.rb b/lib/gitlab/git/repository_mirroring.rb
index 752a91fbb60..7e63a6dc7cb 100644
--- a/lib/gitlab/git/repository_mirroring.rb
+++ b/lib/gitlab/git/repository_mirroring.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Git
module RepositoryMirroring
diff --git a/lib/gitlab/git/tag.rb b/lib/gitlab/git/tag.rb
index bbf2ecdb1fa..ade708d0541 100644
--- a/lib/gitlab/git/tag.rb
+++ b/lib/gitlab/git/tag.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Git
class Tag < Ref
diff --git a/lib/gitlab/git/tree.rb b/lib/gitlab/git/tree.rb
index e0867aeb5a7..51542bcaaa2 100644
--- a/lib/gitlab/git/tree.rb
+++ b/lib/gitlab/git/tree.rb
@@ -1,7 +1,10 @@
+# frozen_string_literal: true
+
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 +18,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/user.rb b/lib/gitlab/git/user.rb
index 338e1a30c45..2c798844798 100644
--- a/lib/gitlab/git/user.rb
+++ b/lib/gitlab/git/user.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Git
class User
diff --git a/lib/gitlab/git/util.rb b/lib/gitlab/git/util.rb
index 4708f22dcb3..03c2c1367b0 100644
--- a/lib/gitlab/git/util.rb
+++ b/lib/gitlab/git/util.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Gitaly note: JV: no RPC's here.
module Gitlab
diff --git a/lib/gitlab/git/version.rb b/lib/gitlab/git/version.rb
index 4bd91898457..64c89656167 100644
--- a/lib/gitlab/git/version.rb
+++ b/lib/gitlab/git/version.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Git
module Version
diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb
index 7fe56979d5c..c43331bed60 100644
--- a/lib/gitlab/git/wiki.rb
+++ b/lib/gitlab/git/wiki.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
module Gitlab
module Git
class Wiki
+ include Gitlab::Git::WrapsGitalyErrors
+
DuplicatePageError = Class.new(StandardError)
OperationError = Class.new(StandardError)
@@ -65,37 +69,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 +109,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 +131,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/wiki_file.rb b/lib/gitlab/git/wiki_file.rb
index 64313bb04e8..c05a5adc00c 100644
--- a/lib/gitlab/git/wiki_file.rb
+++ b/lib/gitlab/git/wiki_file.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Git
class WikiFile
diff --git a/lib/gitlab/git/wiki_page.rb b/lib/gitlab/git/wiki_page.rb
index c4087c9ebdc..f6cac398548 100644
--- a/lib/gitlab/git/wiki_page.rb
+++ b/lib/gitlab/git/wiki_page.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Git
class WikiPage
diff --git a/lib/gitlab/git/wiki_page_version.rb b/lib/gitlab/git/wiki_page_version.rb
index d5e7e70fd31..475a9d4d1b9 100644
--- a/lib/gitlab/git/wiki_page_version.rb
+++ b/lib/gitlab/git/wiki_page_version.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Git
class WikiPageVersion
diff --git a/lib/gitlab/git/wraps_gitaly_errors.rb b/lib/gitlab/git/wraps_gitaly_errors.rb
new file mode 100644
index 00000000000..9963bcfbf1c
--- /dev/null
+++ b/lib/gitlab/git/wraps_gitaly_errors.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+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/git_access.rb b/lib/gitlab/git_access.rb
index 827c04ae035..802fa65dd63 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -9,6 +9,7 @@ module Gitlab
UnauthorizedError = Class.new(StandardError)
NotFoundError = Class.new(StandardError)
ProjectCreationError = Class.new(StandardError)
+ TimeoutError = Class.new(StandardError)
ProjectMovedError = Class.new(NotFoundError)
ERROR_MESSAGES = {
@@ -26,11 +27,18 @@ module Gitlab
cannot_push_to_read_only: "You can't push code to a read-only GitLab instance."
}.freeze
+ INTERNAL_TIMEOUT = 50.seconds.freeze
+ LOG_HEADER = <<~MESSAGE
+ Push operation timed out
+
+ Timing information for debugging purposes:
+ MESSAGE
+
DOWNLOAD_COMMANDS = %w{git-upload-pack git-upload-archive}.freeze
PUSH_COMMANDS = %w{git-receive-pack}.freeze
ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS
- attr_reader :actor, :project, :protocol, :authentication_abilities, :namespace_path, :project_path, :redirected_path, :auth_result_type, :changes
+ attr_reader :actor, :project, :protocol, :authentication_abilities, :namespace_path, :project_path, :redirected_path, :auth_result_type, :changes, :logger
def initialize(actor, project, protocol, authentication_abilities:, namespace_path: nil, project_path: nil, redirected_path: nil, auth_result_type: nil)
@actor = actor
@@ -44,6 +52,7 @@ module Gitlab
end
def check(cmd, changes)
+ @logger = Checks::TimedLogger.new(timeout: INTERNAL_TIMEOUT, header: LOG_HEADER)
@changes = changes
check_protocol!
@@ -269,14 +278,19 @@ module Gitlab
end
def check_single_change_access(change, skip_lfs_integrity_check: false)
- Checks::ChangeAccess.new(
+ change_access = Checks::ChangeAccess.new(
change,
user_access: user_access,
project: project,
skip_authorization: deploy_key?,
skip_lfs_integrity_check: skip_lfs_integrity_check,
- protocol: protocol
- ).exec
+ protocol: protocol,
+ logger: logger
+ )
+
+ change_access.exec
+ rescue Checks::TimedLogger::TimeoutError
+ raise TimeoutError, logger.full_message
end
def deploy_key
diff --git a/lib/gitlab/git_post_receive.rb b/lib/gitlab/git_post_receive.rb
index e731e654f3c..cf2329e489d 100644
--- a/lib/gitlab/git_post_receive.rb
+++ b/lib/gitlab/git_post_receive.rb
@@ -11,8 +11,8 @@ module Gitlab
@changes = deserialize_changes(changes)
end
- def identify(revision)
- super(identifier, project, revision)
+ def identify
+ super(identifier)
end
def changes_refs
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index d99a9f15371..66666b88c15 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -9,11 +9,6 @@ require 'grpc/health/v1/health_services_pb'
module Gitlab
module GitalyClient
include Gitlab::Metrics::Methods
- module MigrationStatus
- DISABLED = 1
- OPT_IN = 2
- OPT_OUT = 3
- end
class TooManyInvocationsError < StandardError
attr_reader :call_site, :invocation_count, :max_call_stack
@@ -31,7 +26,7 @@ module Gitlab
end
end
- SERVER_VERSION_FILE = 'GITALY_SERVER_VERSION'.freeze
+ SERVER_VERSION_FILE = 'GITALY_SERVER_VERSION'
MAXIMUM_GITALY_CALLS = 35
CLIENT_NAME = (Sidekiq.server? ? 'gitlab-sidekiq' : 'gitlab-web').freeze
@@ -43,11 +38,6 @@ module Gitlab
self.query_time = 0
- define_histogram :gitaly_migrate_call_duration_seconds do
- docstring "Gitaly migration call execution timings"
- base_labels gitaly_enabled: nil, feature: nil
- end
-
define_histogram :gitaly_controller_action_duration_seconds do
docstring "Gitaly endpoint histogram by controller and action combination"
base_labels Gitlab::Metrics::Transaction::BASE_LABELS.merge(gitaly_service: nil, rpc: nil)
@@ -126,7 +116,6 @@ module Gitlab
def self.call(storage, service, rpc, request, remote_storage: nil, timeout: nil)
start = Gitlab::Metrics::System.monotonic_time
request_hash = request.is_a?(Google::Protobuf::MessageExts) ? request.to_h : {}
- @current_call_id ||= SecureRandom.uuid
enforce_gitaly_request_limits(:call)
@@ -139,15 +128,13 @@ module Gitlab
ensure
duration = Gitlab::Metrics::System.monotonic_time - start
- # Keep track, seperately, for the performance bar
+ # Keep track, separately, for the performance bar
self.query_time += duration
gitaly_controller_action_duration_seconds.observe(
current_transaction_labels.merge(gitaly_service: service.to_s, rpc: rpc.to_s),
duration)
- add_call_details(id: @current_call_id, feature: service, duration: duration, request: request_hash)
-
- @current_call_id = nil
+ add_call_details(feature: "#{service}##{rpc}", duration: duration, request: request_hash, rpc: rpc)
end
def self.handle_grpc_unavailable!(ex)
@@ -222,7 +209,7 @@ module Gitlab
result
end
- SERVER_FEATURE_FLAGS = %w[gogit_findcommit].freeze
+ SERVER_FEATURE_FLAGS = %w[].freeze
def self.server_feature_flags
SERVER_FEATURE_FLAGS.map do |f|
@@ -237,82 +224,8 @@ module Gitlab
params['gitaly_token'].presence || Gitlab.config.gitaly['token']
end
- # Evaluates whether a feature toggle is on or off
- def self.feature_enabled?(feature_name, status: MigrationStatus::OPT_IN)
- # Disabled features are always off!
- return false if status == MigrationStatus::DISABLED
-
- feature = Feature.get("gitaly_#{feature_name}")
-
- # If the feature has been set, always evaluate
- if Feature.persisted?(feature)
- if feature.percentage_of_time_value > 0
- # Probabilistically enable this feature
- return Random.rand() * 100 < feature.percentage_of_time_value
- end
-
- return feature.enabled?
- end
-
- # If the feature has not been set, the default depends
- # on it's status
- case status
- when MigrationStatus::OPT_OUT
- true
- when MigrationStatus::OPT_IN
- opt_into_all_features? && !explicit_opt_in_required.include?(feature_name)
- else
- false
- end
- rescue => ex
- # During application startup feature lookups in SQL can fail
- Rails.logger.warn "exception while checking Gitaly feature status for #{feature_name}: #{ex}"
- false
- end
-
- # We have a mechanism to let GitLab automatically opt in to all Gitaly
- # features. We want to be able to exclude some features from automatic
- # opt-in. This function has an override in EE.
- def self.explicit_opt_in_required
- []
- end
-
- # opt_into_all_features? returns true when the current environment
- # is one in which we opt into features automatically
- def self.opt_into_all_features?
- Rails.env.development? || ENV["GITALY_FEATURE_DEFAULT_ON"] == "1"
- end
- private_class_method :opt_into_all_features?
-
- def self.migrate(feature, status: MigrationStatus::OPT_IN)
- # Enforce limits at both the `migrate` and `call` sites to ensure that
- # problems are not hidden by a feature being disabled
- enforce_gitaly_request_limits(:migrate)
-
- is_enabled = feature_enabled?(feature, status: status)
- metric_name = feature.to_s
- metric_name += "_gitaly" if is_enabled
-
- Gitlab::Metrics.measure(metric_name) do
- # Some migrate calls wrap other migrate calls
- allow_n_plus_1_calls do
- feature_stack = Thread.current[:gitaly_feature_stack] ||= []
- feature_stack.unshift(feature)
- begin
- start = Gitlab::Metrics::System.monotonic_time
- @current_call_id = SecureRandom.uuid
- call_details = { id: @current_call_id }
- yield is_enabled
- ensure
- total_time = Gitlab::Metrics::System.monotonic_time - start
- gitaly_migrate_call_duration_seconds.observe({ gitaly_enabled: is_enabled, feature: feature }, total_time)
- feature_stack.shift
- Thread.current[:gitaly_feature_stack] = nil if feature_stack.empty?
-
- add_call_details(call_details.merge(feature: feature, duration: total_time))
- end
- end
- end
+ def self.feature_enabled?(feature_name)
+ Feature.enabled?("gitaly_#{feature_name}")
end
# Ensures that Gitaly is not being abuse through n+1 misuse etc
@@ -368,38 +281,20 @@ module Gitlab
end
private_class_method :decrement_call_count
- # Returns an estimate of the number of Gitaly calls made for this
- # request
+ # Returns the of the number of Gitaly calls made for this request
def self.get_request_count
- return 0 unless Gitlab::SafeRequestStore.active?
-
- gitaly_migrate_count = get_call_count("gitaly_migrate_actual")
- gitaly_call_count = get_call_count("gitaly_call_actual")
-
- # Using the maximum of migrate and call_count will provide an
- # indicator of how many Gitaly calls will be made, even
- # before a feature is enabled. This provides us with a single
- # metric, but not an exact number, but this tradeoff is acceptable
- if gitaly_migrate_count > gitaly_call_count
- gitaly_migrate_count
- else
- gitaly_call_count
- end
+ get_call_count("gitaly_call_actual")
end
def self.reset_counts
return unless Gitlab::SafeRequestStore.active?
- %w[migrate call].each do |call_site|
- Gitlab::SafeRequestStore["gitaly_#{call_site}_actual"] = 0
- Gitlab::SafeRequestStore["gitaly_#{call_site}_permitted"] = 0
- end
+ Gitlab::SafeRequestStore["gitaly_call_actual"] = 0
+ Gitlab::SafeRequestStore["gitaly_call_permitted"] = 0
end
def self.add_call_details(details)
- id = details.delete(:id)
-
- return unless id && Gitlab::SafeRequestStore[:peek_enabled]
+ return unless Gitlab::SafeRequestStore[:peek_enabled]
Gitlab::SafeRequestStore['gitaly_call_details'] ||= {}
Gitlab::SafeRequestStore['gitaly_call_details'][id] ||= {}
diff --git a/lib/gitlab/gitaly_client/attributes_bag.rb b/lib/gitlab/gitaly_client/attributes_bag.rb
index 198a1de91c7..3f1a0ef4888 100644
--- a/lib/gitlab/gitaly_client/attributes_bag.rb
+++ b/lib/gitlab/gitaly_client/attributes_bag.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GitalyClient
# This module expects an `ATTRS` const to be defined on the subclass
diff --git a/lib/gitlab/gitaly_client/blob_service.rb b/lib/gitlab/gitaly_client/blob_service.rb
index 1840bf45154..39547328210 100644
--- a/lib/gitlab/gitaly_client/blob_service.rb
+++ b/lib/gitlab/gitaly_client/blob_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GitalyClient
class BlobService
@@ -15,7 +17,7 @@ module Gitlab
)
response = GitalyClient.call(@gitaly_repo.storage_name, :blob_service, :get_blob, request, timeout: GitalyClient.fast_timeout)
- data = ''
+ data = []
blob = nil
response.each do |msg|
if blob.nil?
@@ -27,6 +29,8 @@ module Gitlab
return nil if blob.oid.blank?
+ data = data.join
+
Gitlab::Git::Blob.new(
id: blob.oid,
size: blob.size,
@@ -72,7 +76,7 @@ module Gitlab
GitalyClient::BlobsStitcher.new(response)
end
- def get_new_lfs_pointers(revision, limit, not_in)
+ def get_new_lfs_pointers(revision, limit, not_in, dynamic_timeout = nil)
request = Gitaly::GetNewLFSPointersRequest.new(
repository: @gitaly_repo,
revision: encode_binary(revision),
@@ -85,7 +89,20 @@ module Gitlab
request.not_in_refs += not_in
end
- response = GitalyClient.call(@gitaly_repo.storage_name, :blob_service, :get_new_lfs_pointers, request, timeout: GitalyClient.medium_timeout)
+ timeout =
+ if dynamic_timeout
+ [dynamic_timeout, GitalyClient.medium_timeout].min
+ else
+ GitalyClient.medium_timeout
+ end
+
+ response = GitalyClient.call(
+ @gitaly_repo.storage_name,
+ :blob_service,
+ :get_new_lfs_pointers,
+ request,
+ timeout: timeout
+ )
map_lfs_pointers(response)
end
diff --git a/lib/gitlab/gitaly_client/blobs_stitcher.rb b/lib/gitlab/gitaly_client/blobs_stitcher.rb
index 5ca592ff812..01bab854082 100644
--- a/lib/gitlab/gitaly_client/blobs_stitcher.rb
+++ b/lib/gitlab/gitaly_client/blobs_stitcher.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GitalyClient
class BlobsStitcher
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index 085b2a127a5..4e46cb9f05c 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GitalyClient
class CommitService
@@ -93,7 +95,7 @@ module Gitlab
response = GitalyClient.call(@repository.storage, :commit_service, :tree_entry, request, timeout: GitalyClient.medium_timeout)
entry = nil
- data = ''
+ data = []
response.each do |msg|
if entry.nil?
entry = msg
@@ -103,7 +105,7 @@ module Gitlab
data << msg.data
end
- entry.data = data
+ entry.data = data.join
entry unless entry.oid.blank?
end
@@ -254,7 +256,7 @@ module Gitlab
)
response = GitalyClient.call(@repository.storage, :commit_service, :raw_blame, request, timeout: GitalyClient.medium_timeout)
- response.reduce("") { |memo, msg| memo << msg.data }
+ response.reduce([]) { |memo, msg| memo << msg.data }.join
end
def find_commit(revision)
@@ -345,8 +347,8 @@ module Gitlab
request = Gitaly::ExtractCommitSignatureRequest.new(repository: @gitaly_repo, commit_id: commit_id)
response = GitalyClient.call(@repository.storage, :commit_service, :extract_commit_signature, request)
- signature = ''.b
- signed_text = ''.b
+ signature = +''.b
+ signed_text = +''.b
response.each do |message|
signature << message.signature
@@ -364,7 +366,7 @@ module Gitlab
request = Gitaly::GetCommitSignaturesRequest.new(repository: @gitaly_repo, commit_ids: commit_ids)
response = GitalyClient.call(@repository.storage, :commit_service, :get_commit_signatures, request, timeout: GitalyClient.fast_timeout)
- signatures = Hash.new { |h, k| h[k] = [''.b, ''.b] }
+ signatures = Hash.new { |h, k| h[k] = [+''.b, +''.b] }
current_commit_id = nil
response.each do |message|
@@ -383,7 +385,7 @@ module Gitlab
request = Gitaly::GetCommitMessagesRequest.new(repository: @gitaly_repo, commit_ids: commit_ids)
response = GitalyClient.call(@repository.storage, :commit_service, :get_commit_messages, request, timeout: GitalyClient.fast_timeout)
- messages = Hash.new { |h, k| h[k] = ''.b }
+ messages = Hash.new { |h, k| h[k] = +''.b }
current_commit_id = nil
response.each do |rpc_message|
diff --git a/lib/gitlab/gitaly_client/conflict_files_stitcher.rb b/lib/gitlab/gitaly_client/conflict_files_stitcher.rb
index c275a065bce..0e00f6e8c44 100644
--- a/lib/gitlab/gitaly_client/conflict_files_stitcher.rb
+++ b/lib/gitlab/gitaly_client/conflict_files_stitcher.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GitalyClient
class ConflictFilesStitcher
@@ -17,7 +19,7 @@ module Gitlab
current_file = file_from_gitaly_header(gitaly_file.header)
else
- current_file.raw_content << gitaly_file.content
+ current_file.raw_content = "#{current_file.raw_content}#{gitaly_file.content}"
end
end
end
diff --git a/lib/gitlab/gitaly_client/conflicts_service.rb b/lib/gitlab/gitaly_client/conflicts_service.rb
index aa7e03301f5..6304f998563 100644
--- a/lib/gitlab/gitaly_client/conflicts_service.rb
+++ b/lib/gitlab/gitaly_client/conflicts_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GitalyClient
class ConflictsService
diff --git a/lib/gitlab/gitaly_client/diff.rb b/lib/gitlab/gitaly_client/diff.rb
index af9d674535b..dd192ccde1a 100644
--- a/lib/gitlab/gitaly_client/diff.rb
+++ b/lib/gitlab/gitaly_client/diff.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GitalyClient
class Diff
diff --git a/lib/gitlab/gitaly_client/diff_stitcher.rb b/lib/gitlab/gitaly_client/diff_stitcher.rb
index da243ee2d1a..98d327a7329 100644
--- a/lib/gitlab/gitaly_client/diff_stitcher.rb
+++ b/lib/gitlab/gitaly_client/diff_stitcher.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GitalyClient
class DiffStitcher
@@ -20,7 +22,7 @@ module Gitlab
current_diff = GitalyClient::Diff.new(diff_params)
else
- current_diff.patch += diff_msg.raw_patch_data
+ current_diff.patch = "#{current_diff.patch}#{diff_msg.raw_patch_data}"
end
if diff_msg.end_of_patch
diff --git a/lib/gitlab/gitaly_client/health_check_service.rb b/lib/gitlab/gitaly_client/health_check_service.rb
index 6c1213f5e20..0c495f60633 100644
--- a/lib/gitlab/gitaly_client/health_check_service.rb
+++ b/lib/gitlab/gitaly_client/health_check_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GitalyClient
class HealthCheckService
diff --git a/lib/gitlab/gitaly_client/namespace_service.rb b/lib/gitlab/gitaly_client/namespace_service.rb
index d4e982b649a..f0be3cbebd2 100644
--- a/lib/gitlab/gitaly_client/namespace_service.rb
+++ b/lib/gitlab/gitaly_client/namespace_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GitalyClient
class NamespaceService
diff --git a/lib/gitlab/gitaly_client/notification_service.rb b/lib/gitlab/gitaly_client/notification_service.rb
index 326e6f7dafc..873c3e4086d 100644
--- a/lib/gitlab/gitaly_client/notification_service.rb
+++ b/lib/gitlab/gitaly_client/notification_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GitalyClient
class NotificationService
diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb
index 0f148614b20..c32c2c0b2fb 100644
--- a/lib/gitlab/gitaly_client/operation_service.rb
+++ b/lib/gitlab/gitaly_client/operation_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GitalyClient
class OperationService
@@ -230,6 +232,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 +301,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/queue_enumerator.rb b/lib/gitlab/gitaly_client/queue_enumerator.rb
index b8018029552..3a412102abe 100644
--- a/lib/gitlab/gitaly_client/queue_enumerator.rb
+++ b/lib/gitlab/gitaly_client/queue_enumerator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GitalyClient
class QueueEnumerator
diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb
index 8acc22e809e..d5633d167ac 100644
--- a/lib/gitlab/gitaly_client/ref_service.rb
+++ b/lib/gitlab/gitaly_client/ref_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GitalyClient
class RefService
@@ -218,7 +220,7 @@ module Gitlab
request = Gitaly::GetTagMessagesRequest.new(repository: @gitaly_repo, tag_ids: tag_ids)
response = GitalyClient.call(@repository.storage, :ref_service, :get_tag_messages, request, timeout: GitalyClient.fast_timeout)
- messages = Hash.new { |h, k| h[k] = ''.b }
+ messages = Hash.new { |h, k| h[k] = +''.b }
current_tag_id = nil
response.each do |rpc_message|
diff --git a/lib/gitlab/gitaly_client/remote_service.rb b/lib/gitlab/gitaly_client/remote_service.rb
index 4661448621b..24e8a5e16d3 100644
--- a/lib/gitlab/gitaly_client/remote_service.rb
+++ b/lib/gitlab/gitaly_client/remote_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GitalyClient
class RemoteService
diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb
index 2956ed4b911..f968ebc2cbf 100644
--- a/lib/gitlab/gitaly_client/repository_service.rb
+++ b/lib/gitlab/gitaly_client/repository_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GitalyClient
class RepositoryService
@@ -56,9 +58,9 @@ module Gitlab
request = Gitaly::GetInfoAttributesRequest.new(repository: @gitaly_repo)
response = GitalyClient.call(@storage, :repository_service, :get_info_attributes, request, timeout: GitalyClient.fast_timeout)
- response.each_with_object("") do |message, attributes|
+ response.each_with_object([]) do |message, attributes|
attributes << message.attributes
- end
+ end.join
end
def fetch_remote(remote, ssh_auth:, forced:, no_tags:, timeout:, prune: true)
@@ -349,7 +351,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/gitaly_client/server_service.rb b/lib/gitlab/gitaly_client/server_service.rb
index ad898278353..0ade6942db9 100644
--- a/lib/gitlab/gitaly_client/server_service.rb
+++ b/lib/gitlab/gitaly_client/server_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GitalyClient
# Meant for extraction of server data, and later maybe to perform misc task
diff --git a/lib/gitlab/gitaly_client/storage_service.rb b/lib/gitlab/gitaly_client/storage_service.rb
index 3a26dd58ff4..4edcb0b8ba9 100644
--- a/lib/gitlab/gitaly_client/storage_service.rb
+++ b/lib/gitlab/gitaly_client/storage_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GitalyClient
class StorageService
diff --git a/lib/gitlab/gitaly_client/storage_settings.rb b/lib/gitlab/gitaly_client/storage_settings.rb
index 26d1f53f26c..754cccb6b3f 100644
--- a/lib/gitlab/gitaly_client/storage_settings.rb
+++ b/lib/gitlab/gitaly_client/storage_settings.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GitalyClient
# This is a chokepoint that is meant to help us stop remove all places
diff --git a/lib/gitlab/gitaly_client/util.rb b/lib/gitlab/gitaly_client/util.rb
index 9c19c51d412..dce5d6a8ad0 100644
--- a/lib/gitlab/gitaly_client/util.rb
+++ b/lib/gitlab/gitaly_client/util.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GitalyClient
module Util
diff --git a/lib/gitlab/gitaly_client/wiki_file.rb b/lib/gitlab/gitaly_client/wiki_file.rb
index 47c60c92484..ef2b23732d1 100644
--- a/lib/gitlab/gitaly_client/wiki_file.rb
+++ b/lib/gitlab/gitaly_client/wiki_file.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GitalyClient
class WikiFile
diff --git a/lib/gitlab/gitaly_client/wiki_page.rb b/lib/gitlab/gitaly_client/wiki_page.rb
index a02d15db5dd..757a429fb8a 100644
--- a/lib/gitlab/gitaly_client/wiki_page.rb
+++ b/lib/gitlab/gitaly_client/wiki_page.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GitalyClient
class WikiPage
diff --git a/lib/gitlab/gitaly_client/wiki_service.rb b/lib/gitlab/gitaly_client/wiki_service.rb
index 7c2c228ad01..2b3d622af4d 100644
--- a/lib/gitlab/gitaly_client/wiki_service.rb
+++ b/lib/gitlab/gitaly_client/wiki_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'stringio'
module Gitlab
@@ -139,7 +141,7 @@ module Gitlab
next unless message.name.present? || wiki_file
if wiki_file
- wiki_file.raw_data << message.raw_data
+ wiki_file.raw_data = "#{wiki_file.raw_data}#{message.raw_data}"
else
wiki_file = GitalyClient::WikiFile.new(message.to_h)
# All gRPC strings in a response are frozen, so we get
@@ -160,7 +162,7 @@ module Gitlab
)
response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_get_formatted_data, request)
- response.reduce("") { |memo, msg| memo << msg.data }
+ response.reduce([]) { |memo, msg| memo << msg.data }.join
end
private
diff --git a/lib/gitlab/gitlab_import/client.rb b/lib/gitlab/gitlab_import/client.rb
index 38ef12491df..86474159f8b 100644
--- a/lib/gitlab/gitlab_import/client.rb
+++ b/lib/gitlab/gitlab_import/client.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GitlabImport
class Client
diff --git a/lib/gitlab/gitlab_import/importer.rb b/lib/gitlab/gitlab_import/importer.rb
index 047487f1d24..e84863deba8 100644
--- a/lib/gitlab/gitlab_import/importer.rb
+++ b/lib/gitlab/gitlab_import/importer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GitlabImport
class Importer
@@ -22,22 +24,22 @@ module Gitlab
issues = client.issues(project_identifier)
issues.each do |issue|
- body = @formatter.author_line(issue["author"]["name"])
- body += issue["description"]
+ body = [@formatter.author_line(issue["author"]["name"])]
+ body << issue["description"]
comments = client.issue_comments(project_identifier, issue["iid"])
if comments.any?
- body += @formatter.comments_header
+ body << @formatter.comments_header
end
comments.each do |comment|
- body += @formatter.comment(comment["author"]["name"], comment["created_at"], comment["body"])
+ body << @formatter.comment(comment["author"]["name"], comment["created_at"], comment["body"])
end
project.issues.create!(
iid: issue["iid"],
- description: body,
+ description: body.join,
title: issue["title"],
state: issue["state"],
updated_at: issue["updated_at"],
diff --git a/lib/gitlab/gitlab_import/project_creator.rb b/lib/gitlab/gitlab_import/project_creator.rb
index 430b8c10058..35feea17351 100644
--- a/lib/gitlab/gitlab_import/project_creator.rb
+++ b/lib/gitlab/gitlab_import/project_creator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GitlabImport
class ProjectCreator
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/google_code_import/client.rb b/lib/gitlab/google_code_import/client.rb
index b1dbf554e41..52d714880b5 100644
--- a/lib/gitlab/google_code_import/client.rb
+++ b/lib/gitlab/google_code_import/client.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GoogleCodeImport
class Client
diff --git a/lib/gitlab/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb
index 0c08c0fedaa..1e7203cb82a 100644
--- a/lib/gitlab/google_code_import/importer.rb
+++ b/lib/gitlab/google_code_import/importer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GoogleCodeImport
class Importer
diff --git a/lib/gitlab/google_code_import/project_creator.rb b/lib/gitlab/google_code_import/project_creator.rb
index 326cfcaa8af..eaef85acb98 100644
--- a/lib/gitlab/google_code_import/project_creator.rb
+++ b/lib/gitlab/google_code_import/project_creator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GoogleCodeImport
class ProjectCreator
diff --git a/lib/gitlab/google_code_import/repository.rb b/lib/gitlab/google_code_import/repository.rb
index ad33fc2cad2..19627c8cd35 100644
--- a/lib/gitlab/google_code_import/repository.rb
+++ b/lib/gitlab/google_code_import/repository.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GoogleCodeImport
class Repository
diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb
index 2bc081a6181..31bab20b044 100644
--- a/lib/gitlab/gpg/commit.rb
+++ b/lib/gitlab/gpg/commit.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Gpg
class Commit
diff --git a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb
index 6972bd685f7..d892d27a917 100644
--- a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb
+++ b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Gpg
class InvalidGpgSignatureUpdater
diff --git a/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb b/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb
index 0014ce2689b..9bb1e8fc7a2 100644
--- a/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb
+++ b/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GrapeLogging
module Formatters
@@ -6,7 +8,7 @@ module Gitlab
def call(severity, datetime, _, data)
time = data.delete :time
- data[:params] = utf8_encode_values(data[:params]) if data.has_key?(:params)
+ data[:params] = process_params(data)
attributes = {
time: datetime.utc.iso8601(3),
@@ -20,6 +22,14 @@ module Gitlab
private
+ def process_params(data)
+ return [] unless data.has_key?(:params)
+
+ data[:params]
+ .each_pair
+ .map { |k, v| { key: k, value: utf8_encode_values(v) } }
+ end
+
def utf8_encode_values(data)
case data
when Hash
diff --git a/lib/gitlab/grape_logging/loggers/queue_duration_logger.rb b/lib/gitlab/grape_logging/loggers/queue_duration_logger.rb
index 0adac79f25a..705e23adff2 100644
--- a/lib/gitlab/grape_logging/loggers/queue_duration_logger.rb
+++ b/lib/gitlab/grape_logging/loggers/queue_duration_logger.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# This grape_logging module (https://github.com/aserafin/grape_logging) makes it
# possible to log how much time an API request was queued by Workhorse.
module Gitlab
diff --git a/lib/gitlab/grape_logging/loggers/user_logger.rb b/lib/gitlab/grape_logging/loggers/user_logger.rb
index fa172861967..6caa6c715e7 100644
--- a/lib/gitlab/grape_logging/loggers/user_logger.rb
+++ b/lib/gitlab/grape_logging/loggers/user_logger.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# This grape_logging module (https://github.com/aserafin/grape_logging) makes it
# possible to log the user who performed the Grape API action by retrieving
# the user context from the request environment.
diff --git a/lib/gitlab/graphql/authorize.rb b/lib/gitlab/graphql/authorize.rb
index 93a903915b0..5e48bf9043d 100644
--- a/lib/gitlab/graphql/authorize.rb
+++ b/lib/gitlab/graphql/authorize.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Graphql
# Allow fields to declare permissions their objects must have. The field
diff --git a/lib/gitlab/graphql/authorize/authorize_resource.rb b/lib/gitlab/graphql/authorize/authorize_resource.rb
index 40895686a8a..a56c4f6368d 100644
--- a/lib/gitlab/graphql/authorize/authorize_resource.rb
+++ b/lib/gitlab/graphql/authorize/authorize_resource.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Graphql
module Authorize
diff --git a/lib/gitlab/graphql/authorize/instrumentation.rb b/lib/gitlab/graphql/authorize/instrumentation.rb
index 6cb8e617f62..d638d2b43ee 100644
--- a/lib/gitlab/graphql/authorize/instrumentation.rb
+++ b/lib/gitlab/graphql/authorize/instrumentation.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Graphql
module Authorize
diff --git a/lib/gitlab/graphql/connections.rb b/lib/gitlab/graphql/connections.rb
index 2582ffeb2a8..fbccdfa7b08 100644
--- a/lib/gitlab/graphql/connections.rb
+++ b/lib/gitlab/graphql/connections.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Graphql
module Connections
diff --git a/lib/gitlab/graphql/connections/keyset_connection.rb b/lib/gitlab/graphql/connections/keyset_connection.rb
index 3c0d7e9784a..851054c0393 100644
--- a/lib/gitlab/graphql/connections/keyset_connection.rb
+++ b/lib/gitlab/graphql/connections/keyset_connection.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Graphql
module Connections
diff --git a/lib/gitlab/graphql/errors.rb b/lib/gitlab/graphql/errors.rb
index f8c7ec24be1..fe74549e322 100644
--- a/lib/gitlab/graphql/errors.rb
+++ b/lib/gitlab/graphql/errors.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Graphql
module Errors
diff --git a/lib/gitlab/graphql/expose_permissions.rb b/lib/gitlab/graphql/expose_permissions.rb
index e3779995406..365b7cca24f 100644
--- a/lib/gitlab/graphql/expose_permissions.rb
+++ b/lib/gitlab/graphql/expose_permissions.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Graphql
module ExposePermissions
diff --git a/lib/gitlab/graphql/present.rb b/lib/gitlab/graphql/present.rb
index 2c7b64f1be9..7f69bf601d6 100644
--- a/lib/gitlab/graphql/present.rb
+++ b/lib/gitlab/graphql/present.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Graphql
module Present
diff --git a/lib/gitlab/graphql/present/instrumentation.rb b/lib/gitlab/graphql/present/instrumentation.rb
index f87fd147b15..ab03c40c22d 100644
--- a/lib/gitlab/graphql/present/instrumentation.rb
+++ b/lib/gitlab/graphql/present/instrumentation.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Graphql
module Present
diff --git a/lib/gitlab/graphql/variables.rb b/lib/gitlab/graphql/variables.rb
index ffbaf65b512..b13ea37c21f 100644
--- a/lib/gitlab/graphql/variables.rb
+++ b/lib/gitlab/graphql/variables.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Graphql
class Variables
diff --git a/lib/gitlab/graphs/commits.rb b/lib/gitlab/graphs/commits.rb
index c4ffc19df09..66e1b2e78b4 100644
--- a/lib/gitlab/graphs/commits.rb
+++ b/lib/gitlab/graphs/commits.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Graphs
class Commits
diff --git a/lib/gitlab/hashed_storage/migrator.rb b/lib/gitlab/hashed_storage/migrator.rb
index 4edc251facb..1f29cf10cad 100644
--- a/lib/gitlab/hashed_storage/migrator.rb
+++ b/lib/gitlab/hashed_storage/migrator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module HashedStorage
# Hashed Storage Migrator
diff --git a/lib/gitlab/hashed_storage/rake_helper.rb b/lib/gitlab/hashed_storage/rake_helper.rb
index 22edd5f999d..38f552fab03 100644
--- a/lib/gitlab/hashed_storage/rake_helper.rb
+++ b/lib/gitlab/hashed_storage/rake_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module HashedStorage
module RakeHelper
diff --git a/lib/gitlab/health_checks/base_abstract_check.rb b/lib/gitlab/health_checks/base_abstract_check.rb
index 8b365dab185..1d31f59999c 100644
--- a/lib/gitlab/health_checks/base_abstract_check.rb
+++ b/lib/gitlab/health_checks/base_abstract_check.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module HealthChecks
module BaseAbstractCheck
diff --git a/lib/gitlab/health_checks/db_check.rb b/lib/gitlab/health_checks/db_check.rb
index 08495c0a59e..2bcd25cd3cc 100644
--- a/lib/gitlab/health_checks/db_check.rb
+++ b/lib/gitlab/health_checks/db_check.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module HealthChecks
class DbCheck
diff --git a/lib/gitlab/health_checks/gitaly_check.rb b/lib/gitlab/health_checks/gitaly_check.rb
index 1f623e0b6ec..898733fea5d 100644
--- a/lib/gitlab/health_checks/gitaly_check.rb
+++ b/lib/gitlab/health_checks/gitaly_check.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module HealthChecks
class GitalyCheck
diff --git a/lib/gitlab/health_checks/metric.rb b/lib/gitlab/health_checks/metric.rb
index d62d9136886..62a5216d159 100644
--- a/lib/gitlab/health_checks/metric.rb
+++ b/lib/gitlab/health_checks/metric.rb
@@ -1,3 +1,6 @@
-module Gitlab::HealthChecks # rubocop:disable Naming/FileName
+# rubocop:disable Naming/FileName
+# frozen_string_literal: true
+
+module Gitlab::HealthChecks
Metric = Struct.new(:name, :value, :labels)
end
diff --git a/lib/gitlab/health_checks/prometheus_text_format.rb b/lib/gitlab/health_checks/prometheus_text_format.rb
index b3c759b4730..2a8f9d31cd5 100644
--- a/lib/gitlab/health_checks/prometheus_text_format.rb
+++ b/lib/gitlab/health_checks/prometheus_text_format.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module HealthChecks
class PrometheusTextFormat
diff --git a/lib/gitlab/health_checks/redis/cache_check.rb b/lib/gitlab/health_checks/redis/cache_check.rb
index 2f6c4db12bb..0c8fe83893b 100644
--- a/lib/gitlab/health_checks/redis/cache_check.rb
+++ b/lib/gitlab/health_checks/redis/cache_check.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module HealthChecks
module Redis
diff --git a/lib/gitlab/health_checks/redis/queues_check.rb b/lib/gitlab/health_checks/redis/queues_check.rb
index 63d2882c5b2..b1e33b9f459 100644
--- a/lib/gitlab/health_checks/redis/queues_check.rb
+++ b/lib/gitlab/health_checks/redis/queues_check.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module HealthChecks
module Redis
diff --git a/lib/gitlab/health_checks/redis/redis_check.rb b/lib/gitlab/health_checks/redis/redis_check.rb
index 8ceb0a0aa46..f7e46fce134 100644
--- a/lib/gitlab/health_checks/redis/redis_check.rb
+++ b/lib/gitlab/health_checks/redis/redis_check.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module HealthChecks
module Redis
diff --git a/lib/gitlab/health_checks/redis/shared_state_check.rb b/lib/gitlab/health_checks/redis/shared_state_check.rb
index f1ea1ffe1be..285ac271929 100644
--- a/lib/gitlab/health_checks/redis/shared_state_check.rb
+++ b/lib/gitlab/health_checks/redis/shared_state_check.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module HealthChecks
module Redis
diff --git a/lib/gitlab/health_checks/result.rb b/lib/gitlab/health_checks/result.rb
index e323e2c9723..d32a6980eb8 100644
--- a/lib/gitlab/health_checks/result.rb
+++ b/lib/gitlab/health_checks/result.rb
@@ -1,3 +1,6 @@
-module Gitlab::HealthChecks # rubocop:disable Naming/FileName
+# rubocop:disable Naming/FileName
+# frozen_string_literal: true
+
+module Gitlab::HealthChecks
Result = Struct.new(:success, :message, :labels)
end
diff --git a/lib/gitlab/health_checks/simple_abstract_check.rb b/lib/gitlab/health_checks/simple_abstract_check.rb
index 96945ce5b20..3588260d6eb 100644
--- a/lib/gitlab/health_checks/simple_abstract_check.rb
+++ b/lib/gitlab/health_checks/simple_abstract_check.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module HealthChecks
module SimpleAbstractCheck
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/hook_data/base_builder.rb b/lib/gitlab/hook_data/base_builder.rb
index 1a91301e8be..d54175bce81 100644
--- a/lib/gitlab/hook_data/base_builder.rb
+++ b/lib/gitlab/hook_data/base_builder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module HookData
class BaseBuilder
diff --git a/lib/gitlab/hook_data/issuable_builder.rb b/lib/gitlab/hook_data/issuable_builder.rb
index 65389835ad7..0803df65632 100644
--- a/lib/gitlab/hook_data/issuable_builder.rb
+++ b/lib/gitlab/hook_data/issuable_builder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module HookData
class IssuableBuilder < BaseBuilder
diff --git a/lib/gitlab/hook_data/issue_builder.rb b/lib/gitlab/hook_data/issue_builder.rb
index dd63db969f6..c99353b9d49 100644
--- a/lib/gitlab/hook_data/issue_builder.rb
+++ b/lib/gitlab/hook_data/issue_builder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module HookData
class IssueBuilder < BaseBuilder
diff --git a/lib/gitlab/hook_data/merge_request_builder.rb b/lib/gitlab/hook_data/merge_request_builder.rb
index 3aa6a4f3767..ad38e26e40a 100644
--- a/lib/gitlab/hook_data/merge_request_builder.rb
+++ b/lib/gitlab/hook_data/merge_request_builder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module HookData
class MergeRequestBuilder < BaseBuilder
diff --git a/lib/gitlab/hook_data/note_builder.rb b/lib/gitlab/hook_data/note_builder.rb
index 81873e345d5..ae30ef6364b 100644
--- a/lib/gitlab/hook_data/note_builder.rb
+++ b/lib/gitlab/hook_data/note_builder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module HookData
class NoteBuilder < BaseBuilder
diff --git a/lib/gitlab/hook_data/wiki_page_builder.rb b/lib/gitlab/hook_data/wiki_page_builder.rb
index 59c94a61cf2..67f06b1ca46 100644
--- a/lib/gitlab/hook_data/wiki_page_builder.rb
+++ b/lib/gitlab/hook_data/wiki_page_builder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module HookData
class WikiPageBuilder < BaseBuilder
diff --git a/lib/gitlab/http_io.rb b/lib/gitlab/http_io.rb
index 9d7763fc5ac..e768b8adb12 100644
--- a/lib/gitlab/http_io.rb
+++ b/lib/gitlab/http_io.rb
@@ -161,14 +161,14 @@ module Gitlab
##
# Note: If provider does not return content_range, then we set it as we requested
# Provider: minio
- # - When the file size is larger than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206
- # - When the file size is smaller than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206
+ # - When the file size is larger than requested Content-range, the Content-range is included in responses with Net::HTTPPartialContent 206
+ # - When the file size is smaller than requested Content-range, the Content-range is included in responses with Net::HTTPPartialContent 206
# Provider: AWS
- # - When the file size is larger than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206
- # - When the file size is smaller than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206
+ # - When the file size is larger than requested Content-range, the Content-range is included in responses with Net::HTTPPartialContent 206
+ # - When the file size is smaller than requested Content-range, the Content-range is included in responses with Net::HTTPPartialContent 206
# Provider: GCS
- # - When the file size is larger than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206
- # - When the file size is smaller than requested Content-range, the Content-range is included in responces with Net::HTTPOK 200
+ # - When the file size is larger than requested Content-range, the Content-range is included in responses with Net::HTTPPartialContent 206
+ # - When the file size is smaller than requested Content-range, the Content-range is included in responses with Net::HTTPOK 200
@chunk_range ||= (chunk_start...(chunk_start + @chunk.bytesize))
end
diff --git a/lib/gitlab/i18n/metadata_entry.rb b/lib/gitlab/i18n/metadata_entry.rb
index 36fc1bcdcb7..3764e379681 100644
--- a/lib/gitlab/i18n/metadata_entry.rb
+++ b/lib/gitlab/i18n/metadata_entry.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module I18n
class MetadataEntry
diff --git a/lib/gitlab/i18n/po_linter.rb b/lib/gitlab/i18n/po_linter.rb
index d8e7269a2c2..644ef8c8589 100644
--- a/lib/gitlab/i18n/po_linter.rb
+++ b/lib/gitlab/i18n/po_linter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module I18n
class PoLinter
diff --git a/lib/gitlab/i18n/translation_entry.rb b/lib/gitlab/i18n/translation_entry.rb
index 54adb98f42d..19c10b2e402 100644
--- a/lib/gitlab/i18n/translation_entry.rb
+++ b/lib/gitlab/i18n/translation_entry.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module I18n
class TranslationEntry
diff --git a/lib/gitlab/identifier.rb b/lib/gitlab/identifier.rb
index 28a9fe29465..d5f94ad04f1 100644
--- a/lib/gitlab/identifier.rb
+++ b/lib/gitlab/identifier.rb
@@ -1,13 +1,11 @@
# frozen_string_literal: true
# Detect user based on identifier like
-# key-13 or user-36 or last commit
+# key-13 or user-36
module Gitlab
module Identifier
- def identify(identifier, project = nil, newrev = nil)
- if identifier.blank?
- identify_using_commit(project, newrev)
- elsif identifier =~ /\Auser-\d+\Z/
+ def identify(identifier)
+ if identifier =~ /\Auser-\d+\Z/
# git push over http
identify_using_user(identifier)
elsif identifier =~ /\Akey-\d+\Z/
@@ -16,19 +14,6 @@ module Gitlab
end
end
- # Tries to identify a user based on a commit SHA.
- def identify_using_commit(project, ref)
- return if project.nil? && ref.nil?
-
- commit = project.commit(ref)
-
- return if !commit || !commit.author_email
-
- identify_with_cache(:email, commit.author_email) do
- commit.author
- end
- end
-
# Tries to identify a user based on a user identifier (e.g. "user-123").
# rubocop: disable CodeReuse/ActiveRecord
def identify_using_user(identifier)
diff --git a/lib/gitlab/import/logger.rb b/lib/gitlab/import/logger.rb
index 8414954d141..ab3e822a4e9 100644
--- a/lib/gitlab/import/logger.rb
+++ b/lib/gitlab/import/logger.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Import
class Logger < ::Gitlab::JsonLogger
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/after_export_strategies/base_after_export_strategy.rb b/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb
index 7cbf653dd97..d39b6fe5955 100644
--- a/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb
+++ b/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
module AfterExportStrategies
diff --git a/lib/gitlab/import_export/after_export_strategies/download_notification_strategy.rb b/lib/gitlab/import_export/after_export_strategies/download_notification_strategy.rb
index 4371a7eff56..1b391314a74 100644
--- a/lib/gitlab/import_export/after_export_strategies/download_notification_strategy.rb
+++ b/lib/gitlab/import_export/after_export_strategies/download_notification_strategy.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
module AfterExportStrategies
diff --git a/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb b/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb
index 4f29bdcea2c..b30900f7c61 100644
--- a/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb
+++ b/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
module AfterExportStrategies
diff --git a/lib/gitlab/import_export/after_export_strategy_builder.rb b/lib/gitlab/import_export/after_export_strategy_builder.rb
index 7eabcae2380..37394f46a99 100644
--- a/lib/gitlab/import_export/after_export_strategy_builder.rb
+++ b/lib/gitlab/import_export/after_export_strategy_builder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
class AfterExportStrategyBuilder
diff --git a/lib/gitlab/import_export/attribute_cleaner.rb b/lib/gitlab/import_export/attribute_cleaner.rb
index 7c9fc5c15bb..93b37b7bc5f 100644
--- a/lib/gitlab/import_export/attribute_cleaner.rb
+++ b/lib/gitlab/import_export/attribute_cleaner.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
class AttributeCleaner
diff --git a/lib/gitlab/import_export/attributes_finder.rb b/lib/gitlab/import_export/attributes_finder.rb
index 0c8fda07294..409243e68a5 100644
--- a/lib/gitlab/import_export/attributes_finder.rb
+++ b/lib/gitlab/import_export/attributes_finder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
class AttributesFinder
diff --git a/lib/gitlab/import_export/avatar_restorer.rb b/lib/gitlab/import_export/avatar_restorer.rb
index 17796430811..be1b97bd7a7 100644
--- a/lib/gitlab/import_export/avatar_restorer.rb
+++ b/lib/gitlab/import_export/avatar_restorer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
class AvatarRestorer
diff --git a/lib/gitlab/import_export/avatar_saver.rb b/lib/gitlab/import_export/avatar_saver.rb
index 6ffebf83dd2..47ca898c690 100644
--- a/lib/gitlab/import_export/avatar_saver.rb
+++ b/lib/gitlab/import_export/avatar_saver.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
class AvatarSaver
diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb
index 3adc44f8044..c9e2a6a78d9 100644
--- a/lib/gitlab/import_export/command_line_util.rb
+++ b/lib/gitlab/import_export/command_line_util.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
module CommandLineUtil
diff --git a/lib/gitlab/import_export/error.rb b/lib/gitlab/import_export/error.rb
index 788eedf2686..454dc778b6b 100644
--- a/lib/gitlab/import_export/error.rb
+++ b/lib/gitlab/import_export/error.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
Error = Class.new(StandardError)
diff --git a/lib/gitlab/import_export/file_importer.rb b/lib/gitlab/import_export/file_importer.rb
index 7fd66b4e244..05432f433e7 100644
--- a/lib/gitlab/import_export/file_importer.rb
+++ b/lib/gitlab/import_export/file_importer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
class FileImporter
diff --git a/lib/gitlab/import_export/group_project_object_builder.rb b/lib/gitlab/import_export/group_project_object_builder.rb
index 6c2af770119..1c62591ed5a 100644
--- a/lib/gitlab/import_export/group_project_object_builder.rb
+++ b/lib/gitlab/import_export/group_project_object_builder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
# Given a class, it finds or creates a new object
diff --git a/lib/gitlab/import_export/hash_util.rb b/lib/gitlab/import_export/hash_util.rb
index d4adeeb3797..b6ce89a973b 100644
--- a/lib/gitlab/import_export/hash_util.rb
+++ b/lib/gitlab/import_export/hash_util.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
class HashUtil
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 2bed470514b..b40eac3de9a 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
@@ -153,6 +154,8 @@ excluded_attributes:
- :encrypted_token_iv
- :encrypted_url
- :encrypted_url_iv
+ services:
+ - :template
methods:
labels:
diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb
index 72d5b9b830c..767f1b5de0e 100644
--- a/lib/gitlab/import_export/importer.rb
+++ b/lib/gitlab/import_export/importer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
class Importer
diff --git a/lib/gitlab/import_export/json_hash_builder.rb b/lib/gitlab/import_export/json_hash_builder.rb
index b48f63bcd7e..477499e1688 100644
--- a/lib/gitlab/import_export/json_hash_builder.rb
+++ b/lib/gitlab/import_export/json_hash_builder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
# Generates a hash that conforms with http://apidock.com/rails/Hash/to_json
diff --git a/lib/gitlab/import_export/lfs_restorer.rb b/lib/gitlab/import_export/lfs_restorer.rb
index b28c3c161b7..345c7880e30 100644
--- a/lib/gitlab/import_export/lfs_restorer.rb
+++ b/lib/gitlab/import_export/lfs_restorer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
class LfsRestorer
diff --git a/lib/gitlab/import_export/lfs_saver.rb b/lib/gitlab/import_export/lfs_saver.rb
index 29410e2331c..954f6f00078 100644
--- a/lib/gitlab/import_export/lfs_saver.rb
+++ b/lib/gitlab/import_export/lfs_saver.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
class LfsSaver
diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb
index bcbaf00e11b..6be95a16513 100644
--- a/lib/gitlab/import_export/members_mapper.rb
+++ b/lib/gitlab/import_export/members_mapper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
class MembersMapper
diff --git a/lib/gitlab/import_export/merge_request_parser.rb b/lib/gitlab/import_export/merge_request_parser.rb
index 62a1833b39c..040a70d6775 100644
--- a/lib/gitlab/import_export/merge_request_parser.rb
+++ b/lib/gitlab/import_export/merge_request_parser.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
class MergeRequestParser
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index 3d693d23c99..8cd4efd91cc 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
class ProjectTreeRestorer
@@ -154,7 +156,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/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb
index 5510c0b8b2f..29f2dc80813 100644
--- a/lib/gitlab/import_export/project_tree_saver.rb
+++ b/lib/gitlab/import_export/project_tree_saver.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
class ProjectTreeSaver
diff --git a/lib/gitlab/import_export/reader.rb b/lib/gitlab/import_export/reader.rb
index e621c40fc7a..bc0d18e03fa 100644
--- a/lib/gitlab/import_export/reader.rb
+++ b/lib/gitlab/import_export/reader.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
class Reader
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 2486b1e4921..097c7653754 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
class RelationFactory
@@ -211,7 +213,7 @@ module Gitlab
def update_note_for_missing_author(author_name)
@relation_hash['note'] = '*Blank note*' if @relation_hash['note'].blank?
- @relation_hash['note'] += missing_author_note(@relation_hash['updated_at'], author_name)
+ @relation_hash['note'] = "#{@relation_hash['note']}#{missing_author_note(@relation_hash['updated_at'], author_name)}"
end
def admin_user?
diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb
index 5a9bbceac67..921a06b4023 100644
--- a/lib/gitlab/import_export/repo_restorer.rb
+++ b/lib/gitlab/import_export/repo_restorer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
class RepoRestorer
diff --git a/lib/gitlab/import_export/repo_saver.rb b/lib/gitlab/import_export/repo_saver.rb
index 0c224bd1971..a60618dfcec 100644
--- a/lib/gitlab/import_export/repo_saver.rb
+++ b/lib/gitlab/import_export/repo_saver.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
class RepoSaver
diff --git a/lib/gitlab/import_export/saver.rb b/lib/gitlab/import_export/saver.rb
index 59a74083395..72f575db095 100644
--- a/lib/gitlab/import_export/saver.rb
+++ b/lib/gitlab/import_export/saver.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
class Saver
diff --git a/lib/gitlab/import_export/shared.rb b/lib/gitlab/import_export/shared.rb
index 6d7c36ce38b..c13e6c1d83b 100644
--- a/lib/gitlab/import_export/shared.rb
+++ b/lib/gitlab/import_export/shared.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
class Shared
diff --git a/lib/gitlab/import_export/statistics_restorer.rb b/lib/gitlab/import_export/statistics_restorer.rb
index bcdd9c12c85..3fafb01c37c 100644
--- a/lib/gitlab/import_export/statistics_restorer.rb
+++ b/lib/gitlab/import_export/statistics_restorer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
class StatisticsRestorer
diff --git a/lib/gitlab/import_export/uploads_manager.rb b/lib/gitlab/import_export/uploads_manager.rb
index 8511319cb1c..474e9d45566 100644
--- a/lib/gitlab/import_export/uploads_manager.rb
+++ b/lib/gitlab/import_export/uploads_manager.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
class UploadsManager
diff --git a/lib/gitlab/import_export/uploads_restorer.rb b/lib/gitlab/import_export/uploads_restorer.rb
index b4313ff4cb4..5f422dcbefa 100644
--- a/lib/gitlab/import_export/uploads_restorer.rb
+++ b/lib/gitlab/import_export/uploads_restorer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
class UploadsRestorer < UploadsSaver
diff --git a/lib/gitlab/import_export/uploads_saver.rb b/lib/gitlab/import_export/uploads_saver.rb
index 0275f686c5e..be1066c30b2 100644
--- a/lib/gitlab/import_export/uploads_saver.rb
+++ b/lib/gitlab/import_export/uploads_saver.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
class UploadsSaver
diff --git a/lib/gitlab/import_export/version_checker.rb b/lib/gitlab/import_export/version_checker.rb
index bd3c3ee3b2f..6d978d00ea5 100644
--- a/lib/gitlab/import_export/version_checker.rb
+++ b/lib/gitlab/import_export/version_checker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
class VersionChecker
diff --git a/lib/gitlab/import_export/version_saver.rb b/lib/gitlab/import_export/version_saver.rb
index 7cf88298642..8230c0f1e77 100644
--- a/lib/gitlab/import_export/version_saver.rb
+++ b/lib/gitlab/import_export/version_saver.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
class VersionSaver
diff --git a/lib/gitlab/import_export/wiki_repo_saver.rb b/lib/gitlab/import_export/wiki_repo_saver.rb
index 2fd62c0fc7b..7303bcf61a4 100644
--- a/lib/gitlab/import_export/wiki_repo_saver.rb
+++ b/lib/gitlab/import_export/wiki_repo_saver.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
class WikiRepoSaver < RepoSaver
diff --git a/lib/gitlab/import_export/wiki_restorer.rb b/lib/gitlab/import_export/wiki_restorer.rb
index f33bfb332ab..28b5e7449cd 100644
--- a/lib/gitlab/import_export/wiki_restorer.rb
+++ b/lib/gitlab/import_export/wiki_restorer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ImportExport
class WikiRestorer < RepoRestorer
diff --git a/lib/gitlab/kubernetes/config_map.rb b/lib/gitlab/kubernetes/config_map.rb
index 9e55dae137c..0bcaaa03974 100644
--- a/lib/gitlab/kubernetes/config_map.rb
+++ b/lib/gitlab/kubernetes/config_map.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Kubernetes
class ConfigMap
diff --git a/lib/gitlab/kubernetes/helm.rb b/lib/gitlab/kubernetes/helm.rb
index 4a1bdf34c3e..03d38ec78fd 100644
--- a/lib/gitlab/kubernetes/helm.rb
+++ b/lib/gitlab/kubernetes/helm.rb
@@ -1,7 +1,10 @@
+# frozen_string_literal: true
+
module Gitlab
module Kubernetes
module Helm
- HELM_VERSION = '2.7.2'.freeze
+ HELM_VERSION = '2.11.0'.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..fd3d187cbc3 100644
--- a/lib/gitlab/kubernetes/helm/api.rb
+++ b/lib/gitlab/kubernetes/helm/api.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Kubernetes
module Helm
@@ -54,7 +56,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 +94,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..2bcb428b25d 100644
--- a/lib/gitlab/kubernetes/helm/base_command.rb
+++ b/lib/gitlab/kubernetes/helm/base_command.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Kubernetes
module Helm
@@ -10,13 +12,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/client_command.rb b/lib/gitlab/kubernetes/helm/client_command.rb
new file mode 100644
index 00000000000..9940272a8bf
--- /dev/null
+++ b/lib/gitlab/kubernetes/helm/client_command.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Kubernetes
+ module Helm
+ module ClientCommand
+ def init_command
+ # Here we are always upgrading to the latest version of Tiller when
+ # installing an app. We ensure the helm version stored in the
+ # database is correct by also updating this after transition to
+ # :installed,:updated in Clusters::Concerns::ApplicationStatus
+ 'helm init --upgrade'
+ end
+
+ def wait_for_tiller_command
+ # This is necessary to give Tiller time to restart after upgrade.
+ # Ideally we'd be able to use --wait but cannot because of
+ # https://github.com/helm/helm/issues/4855
+ 'for i in $(seq 1 30); do helm version && break; sleep 1s; echo "Retrying ($i)..."; done'
+ end
+
+ def repository_command
+ ['helm', 'repo', 'add', name, repository].shelljoin if repository
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/kubernetes/helm/init_command.rb b/lib/gitlab/kubernetes/helm/init_command.rb
index c7046a9ea75..88ed8572ffc 100644
--- a/lib/gitlab/kubernetes/helm/init_command.rb
+++ b/lib/gitlab/kubernetes/helm/init_command.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Kubernetes
module Helm
@@ -45,7 +47,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..961485005f7 100644
--- a/lib/gitlab/kubernetes/helm/install_command.rb
+++ b/lib/gitlab/kubernetes/helm/install_command.rb
@@ -1,25 +1,34 @@
+# frozen_string_literal: true
+
module Gitlab
module Kubernetes
module Helm
class InstallCommand
include BaseCommand
+ include ClientCommand
- 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,
+ wait_for_tiller_command,
repository_command,
- script_command
+ repository_update_command,
+ preinstall_command,
+ install_command,
+ postinstall_command
].compact.join("\n")
end
@@ -29,18 +38,22 @@ module Gitlab
private
- def init_command
- 'helm init --client-only >/dev/null'
+ def repository_update_command
+ 'helm repo update' if repository
end
- def repository_command
- ['helm', 'repo', 'add', name, repository].shelljoin if repository
+ def install_command
+ command = ['helm', 'install', chart] + install_command_flags
+
+ command.shelljoin
end
- def script_command
- command = ['helm', 'install', chart] + install_command_flags
+ def preinstall_command
+ preinstall.join("\n") if preinstall
+ end
- command.shelljoin + " >/dev/null\n"
+ 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..75484f80070 100644
--- a/lib/gitlab/kubernetes/helm/pod.rb
+++ b/lib/gitlab/kubernetes/helm/pod.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Kubernetes
module Helm
@@ -25,7 +27,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..9daffc138b5 100644
--- a/lib/gitlab/kubernetes/helm/upgrade_command.rb
+++ b/lib/gitlab/kubernetes/helm/upgrade_command.rb
@@ -5,6 +5,7 @@ module Gitlab
module Helm
class UpgradeCommand
include BaseCommand
+ include ClientCommand
attr_reader :name, :chart, :version, :repository, :files
@@ -20,6 +21,7 @@ module Gitlab
def generate_script
super + [
init_command,
+ wait_for_tiller_command,
repository_command,
script_command
].compact.join("\n")
@@ -35,14 +37,6 @@ module Gitlab
private
- def init_command
- 'helm init --client-only >/dev/null'
- end
-
- def repository_command
- "helm repo add #{name} #{repository}" if repository
- end
-
def script_command
upgrade_flags = "#{optional_version_flag}#{optional_tls_flags}" \
" --reset-values" \
@@ -50,7 +44,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 e88a15b8acd..b947f6b551e 100644
--- a/lib/gitlab/kubernetes/kube_client.rb
+++ b/lib/gitlab/kubernetes/kube_client.rb
@@ -13,11 +13,22 @@ module Gitlab
class KubeClient
include Gitlab::Utils::StrongMemoize
- SUPPORTED_API_GROUPS = [
- 'api',
- 'apis/rbac.authorization.k8s.io',
- 'apis/extensions'
- ].freeze
+ SUPPORTED_API_GROUPS = {
+ core: { group: 'api', version: 'v1' },
+ rbac: { group: 'apis/rbac.authorization.k8s.io', version: 'v1' },
+ extensions: { group: 'apis/extensions', version: 'v1beta1' },
+ knative: { group: 'apis/serving.knative.dev', version: 'v1alpha1' }
+ }.freeze
+
+ SUPPORTED_API_GROUPS.each do |name, params|
+ client_method_name = "#{name}_client".to_sym
+
+ define_method(client_method_name) do
+ strong_memoize(client_method_name) do
+ build_kubeclient(params[:group], params[:version])
+ end
+ end
+ end
# Core API methods delegates to the core api group client
delegate :get_pods,
@@ -62,48 +73,21 @@ module Gitlab
:watch_pod_log,
to: :core_client
- def initialize(api_prefix, api_groups = ['api'], api_version = 'v1', **kubeclient_options)
- raise ArgumentError unless check_api_groups_supported?(api_groups)
+ attr_reader :api_prefix, :kubeclient_options
+ def initialize(api_prefix, **kubeclient_options)
@api_prefix = api_prefix
- @api_groups = api_groups
- @api_version = api_version
@kubeclient_options = kubeclient_options
end
- def discover!
- clients.each(&:discover)
- end
-
- def clients
- hashed_clients.values
- end
-
- def core_client
- hashed_clients['api']
- end
-
- def rbac_client
- hashed_clients['apis/rbac.authorization.k8s.io']
- end
-
- def extensions_client
- hashed_clients['apis/extensions']
- end
-
- def hashed_clients
- strong_memoize(:hashed_clients) do
- @api_groups.map do |api_group|
- api_url = join_api_url(@api_prefix, api_group)
- [api_group, ::Kubeclient::Client.new(api_url, @api_version, **@kubeclient_options)]
- end.to_h
- end
- end
-
private
- def check_api_groups_supported?(api_groups)
- api_groups.all? {|api_group| SUPPORTED_API_GROUPS.include?(api_group) }
+ def build_kubeclient(api_group, api_version)
+ ::Kubeclient::Client.new(
+ join_api_url(api_prefix, api_group),
+ api_version,
+ **kubeclient_options
+ )
end
def join_api_url(api_prefix, api_path)
diff --git a/lib/gitlab/kubernetes/namespace.rb b/lib/gitlab/kubernetes/namespace.rb
index e6ff6160ab9..919f19c86d7 100644
--- a/lib/gitlab/kubernetes/namespace.rb
+++ b/lib/gitlab/kubernetes/namespace.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Kubernetes
class Namespace
@@ -10,9 +12,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/pod.rb b/lib/gitlab/kubernetes/pod.rb
index f3842cdf762..81317e532b2 100644
--- a/lib/gitlab/kubernetes/pod.rb
+++ b/lib/gitlab/kubernetes/pod.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Kubernetes
module Pod
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/legacy_github_import/base_formatter.rb b/lib/gitlab/legacy_github_import/base_formatter.rb
index 11d1300e51e..0b19cf742ed 100644
--- a/lib/gitlab/legacy_github_import/base_formatter.rb
+++ b/lib/gitlab/legacy_github_import/base_formatter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module LegacyGithubImport
class BaseFormatter
diff --git a/lib/gitlab/legacy_github_import/branch_formatter.rb b/lib/gitlab/legacy_github_import/branch_formatter.rb
index 80fe1d67209..1177751457f 100644
--- a/lib/gitlab/legacy_github_import/branch_formatter.rb
+++ b/lib/gitlab/legacy_github_import/branch_formatter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module LegacyGithubImport
class BranchFormatter < BaseFormatter
diff --git a/lib/gitlab/legacy_github_import/client.rb b/lib/gitlab/legacy_github_import/client.rb
index d8ed0ebca9d..bc952147667 100644
--- a/lib/gitlab/legacy_github_import/client.rb
+++ b/lib/gitlab/legacy_github_import/client.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module LegacyGithubImport
class Client
diff --git a/lib/gitlab/legacy_github_import/comment_formatter.rb b/lib/gitlab/legacy_github_import/comment_formatter.rb
index d2c7a8ae9f4..d83cc4f6b3c 100644
--- a/lib/gitlab/legacy_github_import/comment_formatter.rb
+++ b/lib/gitlab/legacy_github_import/comment_formatter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module LegacyGithubImport
class CommentFormatter < BaseFormatter
diff --git a/lib/gitlab/legacy_github_import/importer.rb b/lib/gitlab/legacy_github_import/importer.rb
index c5bde681365..43695451b87 100644
--- a/lib/gitlab/legacy_github_import/importer.rb
+++ b/lib/gitlab/legacy_github_import/importer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module LegacyGithubImport
class Importer
diff --git a/lib/gitlab/legacy_github_import/issuable_formatter.rb b/lib/gitlab/legacy_github_import/issuable_formatter.rb
index 7db4a54267e..1a0aefbbd62 100644
--- a/lib/gitlab/legacy_github_import/issuable_formatter.rb
+++ b/lib/gitlab/legacy_github_import/issuable_formatter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module LegacyGithubImport
class IssuableFormatter < BaseFormatter
diff --git a/lib/gitlab/legacy_github_import/issue_formatter.rb b/lib/gitlab/legacy_github_import/issue_formatter.rb
index 4c8825ccf19..2f46e2e30d1 100644
--- a/lib/gitlab/legacy_github_import/issue_formatter.rb
+++ b/lib/gitlab/legacy_github_import/issue_formatter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module LegacyGithubImport
class IssueFormatter < IssuableFormatter
diff --git a/lib/gitlab/legacy_github_import/label_formatter.rb b/lib/gitlab/legacy_github_import/label_formatter.rb
index e9663650903..89200e794d8 100644
--- a/lib/gitlab/legacy_github_import/label_formatter.rb
+++ b/lib/gitlab/legacy_github_import/label_formatter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module LegacyGithubImport
class LabelFormatter < BaseFormatter
diff --git a/lib/gitlab/legacy_github_import/milestone_formatter.rb b/lib/gitlab/legacy_github_import/milestone_formatter.rb
index a565294384d..2fe1b4258d3 100644
--- a/lib/gitlab/legacy_github_import/milestone_formatter.rb
+++ b/lib/gitlab/legacy_github_import/milestone_formatter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module LegacyGithubImport
class MilestoneFormatter < BaseFormatter
diff --git a/lib/gitlab/legacy_github_import/project_creator.rb b/lib/gitlab/legacy_github_import/project_creator.rb
index 5e96eb16754..ca1a1b8e9bd 100644
--- a/lib/gitlab/legacy_github_import/project_creator.rb
+++ b/lib/gitlab/legacy_github_import/project_creator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module LegacyGithubImport
class ProjectCreator
diff --git a/lib/gitlab/legacy_github_import/pull_request_formatter.rb b/lib/gitlab/legacy_github_import/pull_request_formatter.rb
index 94c2e99066a..5b847f13d4a 100644
--- a/lib/gitlab/legacy_github_import/pull_request_formatter.rb
+++ b/lib/gitlab/legacy_github_import/pull_request_formatter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module LegacyGithubImport
class PullRequestFormatter < IssuableFormatter
diff --git a/lib/gitlab/legacy_github_import/release_formatter.rb b/lib/gitlab/legacy_github_import/release_formatter.rb
index 3ed9d4f76da..8c0c17780ca 100644
--- a/lib/gitlab/legacy_github_import/release_formatter.rb
+++ b/lib/gitlab/legacy_github_import/release_formatter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module LegacyGithubImport
class ReleaseFormatter < BaseFormatter
diff --git a/lib/gitlab/legacy_github_import/user_formatter.rb b/lib/gitlab/legacy_github_import/user_formatter.rb
index 3794380e2d0..ec0e221b1ff 100644
--- a/lib/gitlab/legacy_github_import/user_formatter.rb
+++ b/lib/gitlab/legacy_github_import/user_formatter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module LegacyGithubImport
class UserFormatter
diff --git a/lib/gitlab/legacy_github_import/wiki_formatter.rb b/lib/gitlab/legacy_github_import/wiki_formatter.rb
index 27f45875c7c..ea52be5ee0f 100644
--- a/lib/gitlab/legacy_github_import/wiki_formatter.rb
+++ b/lib/gitlab/legacy_github_import/wiki_formatter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module LegacyGithubImport
class WikiFormatter
diff --git a/lib/gitlab/manifest_import/manifest.rb b/lib/gitlab/manifest_import/manifest.rb
index 4d6034fb956..7208fe5bbc5 100644
--- a/lib/gitlab/manifest_import/manifest.rb
+++ b/lib/gitlab/manifest_import/manifest.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Class to parse manifest file and build a list of repositories for import
#
# <manifest>
@@ -63,7 +65,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/manifest_import/project_creator.rb b/lib/gitlab/manifest_import/project_creator.rb
index b5967c93735..837d65e5f7c 100644
--- a/lib/gitlab/manifest_import/project_creator.rb
+++ b/lib/gitlab/manifest_import/project_creator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module ManifestImport
class ProjectCreator
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/metrics/background_transaction.rb b/lib/gitlab/metrics/background_transaction.rb
index 5919ebb1493..fe1722b1095 100644
--- a/lib/gitlab/metrics/background_transaction.rb
+++ b/lib/gitlab/metrics/background_transaction.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Metrics
class BackgroundTransaction < Transaction
diff --git a/lib/gitlab/metrics/delta.rb b/lib/gitlab/metrics/delta.rb
index bcf28eed84d..ab2d9e46390 100644
--- a/lib/gitlab/metrics/delta.rb
+++ b/lib/gitlab/metrics/delta.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Metrics
# Class for calculating the difference between two numeric values.
diff --git a/lib/gitlab/metrics/influx_db.rb b/lib/gitlab/metrics/influx_db.rb
index ce9d3ec3de4..1359e973590 100644
--- a/lib/gitlab/metrics/influx_db.rb
+++ b/lib/gitlab/metrics/influx_db.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Metrics
module InfluxDb
diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb
index 023e9963493..651e241362c 100644
--- a/lib/gitlab/metrics/instrumentation.rb
+++ b/lib/gitlab/metrics/instrumentation.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Metrics
# Module for instrumenting methods.
diff --git a/lib/gitlab/metrics/method_call.rb b/lib/gitlab/metrics/method_call.rb
index f3290e3149c..85438011cb9 100644
--- a/lib/gitlab/metrics/method_call.rb
+++ b/lib/gitlab/metrics/method_call.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Metrics
# Class for tracking timing information about method calls
diff --git a/lib/gitlab/metrics/methods.rb b/lib/gitlab/metrics/methods.rb
index f79eb0cd1bf..447d03bfca4 100644
--- a/lib/gitlab/metrics/methods.rb
+++ b/lib/gitlab/metrics/methods.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# rubocop:disable Style/ClassVars
module Gitlab
diff --git a/lib/gitlab/metrics/methods/metric_options.rb b/lib/gitlab/metrics/methods/metric_options.rb
index 70e122d4e15..8e6ceb74c09 100644
--- a/lib/gitlab/metrics/methods/metric_options.rb
+++ b/lib/gitlab/metrics/methods/metric_options.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Metrics
module Methods
diff --git a/lib/gitlab/metrics/metric.rb b/lib/gitlab/metrics/metric.rb
index bd0afe53c51..9e4d70a71ff 100644
--- a/lib/gitlab/metrics/metric.rb
+++ b/lib/gitlab/metrics/metric.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Metrics
# Class for storing details of a single metric (label, value, etc).
diff --git a/lib/gitlab/metrics/null_metric.rb b/lib/gitlab/metrics/null_metric.rb
index aabada5c21a..7dbd2a1f8e3 100644
--- a/lib/gitlab/metrics/null_metric.rb
+++ b/lib/gitlab/metrics/null_metric.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Metrics
# Mocks ::Prometheus::Client::Metric and all derived metrics
diff --git a/lib/gitlab/metrics/prometheus.rb b/lib/gitlab/metrics/prometheus.rb
index d41a855bff1..cab1edab48f 100644
--- a/lib/gitlab/metrics/prometheus.rb
+++ b/lib/gitlab/metrics/prometheus.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'prometheus/client'
module Gitlab
diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb
index 2d45765df3f..9aa97515961 100644
--- a/lib/gitlab/metrics/rack_middleware.rb
+++ b/lib/gitlab/metrics/rack_middleware.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Metrics
# Rack middleware for tracking Rails and Grape requests.
diff --git a/lib/gitlab/metrics/requests_rack_middleware.rb b/lib/gitlab/metrics/requests_rack_middleware.rb
index 0dc19f31d03..74c956ab5af 100644
--- a/lib/gitlab/metrics/requests_rack_middleware.rb
+++ b/lib/gitlab/metrics/requests_rack_middleware.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Metrics
class RequestsRackMiddleware
diff --git a/lib/gitlab/metrics/samplers/base_sampler.rb b/lib/gitlab/metrics/samplers/base_sampler.rb
index 37f90c4673d..6a062e93f0f 100644
--- a/lib/gitlab/metrics/samplers/base_sampler.rb
+++ b/lib/gitlab/metrics/samplers/base_sampler.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'logger'
module Gitlab
diff --git a/lib/gitlab/metrics/samplers/influx_sampler.rb b/lib/gitlab/metrics/samplers/influx_sampler.rb
index ad97632e4eb..c4c38b23a55 100644
--- a/lib/gitlab/metrics/samplers/influx_sampler.rb
+++ b/lib/gitlab/metrics/samplers/influx_sampler.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Metrics
module Samplers
diff --git a/lib/gitlab/metrics/samplers/ruby_sampler.rb b/lib/gitlab/metrics/samplers/ruby_sampler.rb
index 7b2b3bedf04..232a58a7d69 100644
--- a/lib/gitlab/metrics/samplers/ruby_sampler.rb
+++ b/lib/gitlab/metrics/samplers/ruby_sampler.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'prometheus/client/support/unicorn'
module Gitlab
diff --git a/lib/gitlab/metrics/samplers/unicorn_sampler.rb b/lib/gitlab/metrics/samplers/unicorn_sampler.rb
index ea325651fbb..4c4ec026823 100644
--- a/lib/gitlab/metrics/samplers/unicorn_sampler.rb
+++ b/lib/gitlab/metrics/samplers/unicorn_sampler.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Metrics
module Samplers
diff --git a/lib/gitlab/metrics/sidekiq_metrics_exporter.rb b/lib/gitlab/metrics/sidekiq_metrics_exporter.rb
index 47b4af5d649..56e106b9612 100644
--- a/lib/gitlab/metrics/sidekiq_metrics_exporter.rb
+++ b/lib/gitlab/metrics/sidekiq_metrics_exporter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'webrick'
require 'prometheus/client/rack/exporter'
diff --git a/lib/gitlab/metrics/sidekiq_middleware.rb b/lib/gitlab/metrics/sidekiq_middleware.rb
index df4bdf16847..0b4485feea9 100644
--- a/lib/gitlab/metrics/sidekiq_middleware.rb
+++ b/lib/gitlab/metrics/sidekiq_middleware.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Metrics
# Sidekiq middleware for tracking jobs.
diff --git a/lib/gitlab/metrics/subscribers/action_view.rb b/lib/gitlab/metrics/subscribers/action_view.rb
index b600e8a2a50..c068f8017fd 100644
--- a/lib/gitlab/metrics/subscribers/action_view.rb
+++ b/lib/gitlab/metrics/subscribers/action_view.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Metrics
module Subscribers
diff --git a/lib/gitlab/metrics/subscribers/active_record.rb b/lib/gitlab/metrics/subscribers/active_record.rb
index 04107296ae3..a02dd850582 100644
--- a/lib/gitlab/metrics/subscribers/active_record.rb
+++ b/lib/gitlab/metrics/subscribers/active_record.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Metrics
module Subscribers
diff --git a/lib/gitlab/metrics/subscribers/rails_cache.rb b/lib/gitlab/metrics/subscribers/rails_cache.rb
index 250897a79c2..f633e1a9d7c 100644
--- a/lib/gitlab/metrics/subscribers/rails_cache.rb
+++ b/lib/gitlab/metrics/subscribers/rails_cache.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Metrics
module Subscribers
diff --git a/lib/gitlab/metrics/system.rb b/lib/gitlab/metrics/system.rb
index e60e245cf89..426496855e3 100644
--- a/lib/gitlab/metrics/system.rb
+++ b/lib/gitlab/metrics/system.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Metrics
# Module for gathering system/process statistics such as the memory usage.
diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb
index 9f903e96585..468d7cb56fc 100644
--- a/lib/gitlab/metrics/transaction.rb
+++ b/lib/gitlab/metrics/transaction.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Metrics
# Class for storing metrics information of a single transaction.
diff --git a/lib/gitlab/metrics/web_transaction.rb b/lib/gitlab/metrics/web_transaction.rb
index 723ca576aab..b2a43d46fb2 100644
--- a/lib/gitlab/metrics/web_transaction.rb
+++ b/lib/gitlab/metrics/web_transaction.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Metrics
class WebTransaction < Transaction
@@ -40,7 +42,7 @@ module Gitlab
# increasing the cardinality of our metrics, we limit the number of
# possible suffixes.
if suffix && ALLOWED_SUFFIXES.include?(suffix)
- action += ".#{suffix}"
+ action = "#{action}.#{suffix}"
end
{ controller: controller.class.name, action: action }
diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb
index 1fd8f147b44..d1a87c3b3bb 100644
--- a/lib/gitlab/middleware/go.rb
+++ b/lib/gitlab/middleware/go.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# A dumb middleware that returns a Go HTML document if the go-get=1 query string
# is used irrespective if the namespace/project exists
module Gitlab
@@ -38,7 +40,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/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb
index 10cb1e7127e..84c2f0d5720 100644
--- a/lib/gitlab/middleware/multipart.rb
+++ b/lib/gitlab/middleware/multipart.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Gitlab::Middleware::Multipart - a Rack::Multipart replacement
#
# Rack::Multipart leaves behind tempfiles in /tmp and uses valuable Ruby
diff --git a/lib/gitlab/middleware/rails_queue_duration.rb b/lib/gitlab/middleware/rails_queue_duration.rb
index bc70b2459ef..96c6a0a7d28 100644
--- a/lib/gitlab/middleware/rails_queue_duration.rb
+++ b/lib/gitlab/middleware/rails_queue_duration.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# This Rack middleware is intended to measure the latency between
# gitlab-workhorse forwarding a request to the Rails application and the
# time this middleware is reached.
diff --git a/lib/gitlab/middleware/read_only.rb b/lib/gitlab/middleware/read_only.rb
index 7f63e39b3aa..83c52a6c6e0 100644
--- a/lib/gitlab/middleware/read_only.rb
+++ b/lib/gitlab/middleware/read_only.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Middleware
class ReadOnly
diff --git a/lib/gitlab/middleware/read_only/controller.rb b/lib/gitlab/middleware/read_only/controller.rb
index 8dca431c005..89941a9efa0 100644
--- a/lib/gitlab/middleware/read_only/controller.rb
+++ b/lib/gitlab/middleware/read_only/controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Middleware
class ReadOnly
diff --git a/lib/gitlab/middleware/release_env.rb b/lib/gitlab/middleware/release_env.rb
index bfe8e113b5e..849cf8f759b 100644
--- a/lib/gitlab/middleware/release_env.rb
+++ b/lib/gitlab/middleware/release_env.rb
@@ -1,4 +1,7 @@
-module Gitlab # rubocop:disable Naming/FileName
+# rubocop:disable Naming/FileName
+# frozen_string_literal: true
+
+module Gitlab
module Middleware
# Some of middleware would hold env for no good reason even after the
# request had already been processed, and we could not garbage collect
diff --git a/lib/gitlab/middleware/static.rb b/lib/gitlab/middleware/static.rb
index aa1e9dc0fdb..972fed2134c 100644
--- a/lib/gitlab/middleware/static.rb
+++ b/lib/gitlab/middleware/static.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Middleware
class Static < ActionDispatch::Static
diff --git a/lib/gitlab/patch/draw_route.rb b/lib/gitlab/patch/draw_route.rb
new file mode 100644
index 00000000000..b00244a6e04
--- /dev/null
+++ b/lib/gitlab/patch/draw_route.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+# We're patching `ActionDispatch::Routing::Mapper` in
+# config/initializers/routing_draw.rb
+module Gitlab
+ module Patch
+ module DrawRoute
+ RoutesNotFound = Class.new(StandardError)
+
+ def draw(routes_name)
+ drawn_any = draw_ce(routes_name) | draw_ee(routes_name)
+
+ drawn_any || raise(RoutesNotFound.new("Cannot find #{routes_name}"))
+ end
+
+ def draw_ce(routes_name)
+ draw_route(route_path("config/routes/#{routes_name}.rb"))
+ end
+
+ def draw_ee(_)
+ true
+ end
+
+ def route_path(routes_name)
+ Rails.root.join(routes_name)
+ end
+
+ def draw_route(path)
+ if File.exist?(path)
+ instance_eval(File.read(path))
+ true
+ else
+ false
+ end
+ end
+ end
+ end
+end
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..536fc9dae3a
--- /dev/null
+++ b/lib/gitlab/private_commit_email.rb
@@ -0,0 +1,32 @@
+# 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 user_ids_for_emails(emails)
+ emails.map { |email| user_id_for_email(email) }.compact.uniq
+ 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/repository_cache.rb b/lib/gitlab/repository_cache.rb
index 56007574b1b..6b0808f5304 100644
--- a/lib/gitlab/repository_cache.rb
+++ b/lib/gitlab/repository_cache.rb
@@ -7,13 +7,13 @@ module Gitlab
def initialize(repository, extra_namespace: nil, backend: Rails.cache)
@repository = repository
- @namespace = "#{repository.full_path}:#{repository.project.id}"
+ @namespace = "project:#{repository.project.id}"
@namespace = "#{@namespace}:#{extra_namespace}" if extra_namespace
@backend = backend
end
def cache_key(type)
- "#{type}:#{namespace}"
+ "#{namespace}:#{type}"
end
def expire(key)
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/url_blocker.rb b/lib/gitlab/url_blocker.rb
index 7735b736689..86efe8ad114 100644
--- a/lib/gitlab/url_blocker.rb
+++ b/lib/gitlab/url_blocker.rb
@@ -32,6 +32,7 @@ module Gitlab
end
validate_localhost!(addrs_info) unless allow_localhost
+ validate_loopback!(addrs_info) unless allow_localhost
validate_local_network!(addrs_info) unless allow_local_network
validate_link_local!(addrs_info) unless allow_local_network
@@ -86,6 +87,12 @@ module Gitlab
raise BlockedUrlError, "Requests to localhost are not allowed"
end
+ def validate_loopback!(addrs_info)
+ return unless addrs_info.any? { |addr| addr.ipv4_loopback? || addr.ipv6_loopback? }
+
+ raise BlockedUrlError, "Requests to loopback addresses are not allowed"
+ end
+
def validate_local_network!(addrs_info)
return unless addrs_info.any? { |addr| addr.ipv4_private? || addr.ipv6_sitelocal? }
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/gitlab/view/presenter/base.rb b/lib/gitlab/view/presenter/base.rb
index 36162faa1eb..c3fd6d317aa 100644
--- a/lib/gitlab/view/presenter/base.rb
+++ b/lib/gitlab/view/presenter/base.rb
@@ -11,8 +11,8 @@ module Gitlab
attr_reader :subject
- def can?(user, action, overriden_subject = nil)
- super(user, action, overriden_subject || subject)
+ def can?(user, action, overridden_subject = nil)
+ super(user, action, overridden_subject || subject)
end
# delegate all #can? queries to the subject
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/lib/tasks/gitlab/ldap.rake b/lib/tasks/gitlab/ldap.rake
index c66a2a263dc..0459de27c96 100644
--- a/lib/tasks/gitlab/ldap.rake
+++ b/lib/tasks/gitlab/ldap.rake
@@ -1,7 +1,7 @@
namespace :gitlab do
namespace :ldap do
desc 'GitLab | LDAP | Rename provider'
- task :rename_provider, [:old_provider, :new_provider] => :environment do |_, args|
+ task :rename_provider, [:old_provider, :new_provider] => :gitlab_environment do |_, args|
old_provider = args[:old_provider] ||
prompt('What is the old provider? Ex. \'ldapmain\': '.color(:blue))
new_provider = args[:new_provider] ||
diff --git a/lib/tasks/tokens.rake b/lib/tasks/tokens.rake
index 81829668de8..eec024f9bbb 100644
--- a/lib/tasks/tokens.rake
+++ b/lib/tasks/tokens.rake
@@ -1,4 +1,7 @@
require_relative '../../app/models/concerns/token_authenticatable.rb'
+require_relative '../../app/models/concerns/token_authenticatable_strategies/base.rb'
+require_relative '../../app/models/concerns/token_authenticatable_strategies/insecure.rb'
+require_relative '../../app/models/concerns/token_authenticatable_strategies/digest.rb'
namespace :tokens do
desc "Reset all GitLab incoming email tokens"
@@ -26,13 +29,6 @@ class TmpUser < ActiveRecord::Base
self.table_name = 'users'
- def reset_incoming_email_token!
- write_new_token(:incoming_email_token)
- save!(validate: false)
- end
-
- def reset_feed_token!
- write_new_token(:feed_token)
- save!(validate: false)
- end
+ add_authentication_token_field :incoming_email_token, token_generator: -> { SecureRandom.hex.to_i(16).to_s(36) }
+ add_authentication_token_field :feed_token
end
diff --git a/locale/ar_SA/gitlab.po b/locale/ar_SA/gitlab.po
index 903d72d9ea8..4a2b56f2806 100644
--- a/locale/ar_SA/gitlab.po
+++ b/locale/ar_SA/gitlab.po
@@ -681,7 +681,7 @@ msgstr ""
msgid "An error occured whilst fetching the job trace."
msgstr ""
-msgid "An error occured whilst fetching the latest pipline."
+msgid "An error occured whilst fetching the latest pipeline."
msgstr ""
msgid "An error occured whilst loading all the files."
@@ -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..6c6eeeb6580 100644
--- a/locale/bg/gitlab.po
+++ b/locale/bg/gitlab.po
@@ -561,7 +561,7 @@ msgstr ""
msgid "An error occured whilst fetching the job trace."
msgstr ""
-msgid "An error occured whilst fetching the latest pipline."
+msgid "An error occured whilst fetching the latest pipeline."
msgstr ""
msgid "An error occured whilst loading all the files."
@@ -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..a957023bb25 100644
--- a/locale/ca_ES/gitlab.po
+++ b/locale/ca_ES/gitlab.po
@@ -561,7 +561,7 @@ msgstr ""
msgid "An error occured whilst fetching the job trace."
msgstr ""
-msgid "An error occured whilst fetching the latest pipline."
+msgid "An error occured whilst fetching the latest pipeline."
msgstr ""
msgid "An error occured whilst loading all the files."
@@ -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..9801999299f 100644
--- a/locale/cs_CZ/gitlab.po
+++ b/locale/cs_CZ/gitlab.po
@@ -621,7 +621,7 @@ msgstr ""
msgid "An error occured whilst fetching the job trace."
msgstr ""
-msgid "An error occured whilst fetching the latest pipline."
+msgid "An error occured whilst fetching the latest pipeline."
msgstr ""
msgid "An error occured whilst loading all the files."
@@ -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..9c7b37f7f18 100644
--- a/locale/da_DK/gitlab.po
+++ b/locale/da_DK/gitlab.po
@@ -561,7 +561,7 @@ msgstr ""
msgid "An error occured whilst fetching the job trace."
msgstr ""
-msgid "An error occured whilst fetching the latest pipline."
+msgid "An error occured whilst fetching the latest pipeline."
msgstr ""
msgid "An error occured whilst loading all the files."
@@ -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..07568765abb 100644
--- a/locale/de/gitlab.po
+++ b/locale/de/gitlab.po
@@ -561,7 +561,7 @@ msgstr ""
msgid "An error occured whilst fetching the job trace."
msgstr ""
-msgid "An error occured whilst fetching the latest pipline."
+msgid "An error occured whilst fetching the latest pipeline."
msgstr ""
msgid "An error occured whilst loading all the files."
@@ -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..84ef902d5e1 100644
--- a/locale/eo/gitlab.po
+++ b/locale/eo/gitlab.po
@@ -561,7 +561,7 @@ msgstr ""
msgid "An error occured whilst fetching the job trace."
msgstr ""
-msgid "An error occured whilst fetching the latest pipline."
+msgid "An error occured whilst fetching the latest pipeline."
msgstr ""
msgid "An error occured whilst loading all the files."
@@ -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..32e495695c6 100644
--- a/locale/es/gitlab.po
+++ b/locale/es/gitlab.po
@@ -561,7 +561,7 @@ msgstr ""
msgid "An error occured whilst fetching the job trace."
msgstr ""
-msgid "An error occured whilst fetching the latest pipline."
+msgid "An error occured whilst fetching the latest pipeline."
msgstr ""
msgid "An error occured whilst loading all the files."
@@ -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..bc6b3b67d42 100644
--- a/locale/et_EE/gitlab.po
+++ b/locale/et_EE/gitlab.po
@@ -561,7 +561,7 @@ msgstr ""
msgid "An error occured whilst fetching the job trace."
msgstr ""
-msgid "An error occured whilst fetching the latest pipline."
+msgid "An error occured whilst fetching the latest pipeline."
msgstr ""
msgid "An error occured whilst loading all the files."
@@ -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..ecbf47aa928 100644
--- a/locale/fil_PH/gitlab.po
+++ b/locale/fil_PH/gitlab.po
@@ -561,7 +561,7 @@ msgstr ""
msgid "An error occured whilst fetching the job trace."
msgstr ""
-msgid "An error occured whilst fetching the latest pipline."
+msgid "An error occured whilst fetching the latest pipeline."
msgstr ""
msgid "An error occured whilst loading all the files."
@@ -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..6e89ec73142 100644
--- a/locale/fr/gitlab.po
+++ b/locale/fr/gitlab.po
@@ -561,7 +561,7 @@ msgstr "Une erreur est survenue lors de la création de la nouvelle branche."
msgid "An error occured whilst fetching the job trace."
msgstr "Une erreur est survenue pendant le rapatriement de la trace de la tâche."
-msgid "An error occured whilst fetching the latest pipline."
+msgid "An error occured whilst fetching the latest pipeline."
msgstr "Une erreur est survenue lors du rapatriement de dernier pipeline."
msgid "An error occured whilst loading all the files."
@@ -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 26270595c6a..c021d1aad1c 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 ""
@@ -492,7 +501,7 @@ msgstr ""
msgid "An error occured whilst fetching the job trace."
msgstr ""
-msgid "An error occured whilst fetching the latest pipline."
+msgid "An error occured whilst fetching the latest pipeline."
msgstr ""
msgid "An error occured whilst loading all the files."
@@ -648,6 +657,9 @@ msgstr ""
msgid "Are you sure you want to reset the health check token?"
msgstr ""
+msgid "Are you sure you want to stop this environment?"
+msgstr ""
+
msgid "Are you sure?"
msgstr ""
@@ -1343,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 ""
@@ -1451,6 +1475,9 @@ msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine project"
msgstr ""
+msgid "ClusterIntegration|Group cluster"
+msgstr ""
+
msgid "ClusterIntegration|Helm Tiller"
msgstr ""
@@ -1493,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..."
@@ -1511,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"
@@ -1520,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"
@@ -1568,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 ""
@@ -1580,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."
@@ -1652,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 ""
@@ -1718,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 ""
@@ -1971,6 +2007,9 @@ msgstr ""
msgid "Copy file path to clipboard"
msgstr ""
+msgid "Copy link"
+msgstr ""
+
msgid "Copy reference to clipboard"
msgstr ""
@@ -1983,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 ""
@@ -2091,6 +2133,9 @@ msgstr ""
msgid "Custom CI config path"
msgstr ""
+msgid "Custom hostname (for private commit emails)"
+msgstr ""
+
msgid "Custom notification events"
msgstr ""
@@ -2166,7 +2211,10 @@ 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 "Define environments in the deploy stage(s) in <code>.gitlab-ci.yml</code> to track deployments here."
+msgstr ""
+
+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."
@@ -2187,6 +2235,9 @@ msgstr ""
msgid "Delete Snippet"
msgstr ""
+msgid "Delete comment"
+msgstr ""
+
msgid "Delete list"
msgstr ""
@@ -2324,6 +2375,12 @@ msgstr ""
msgid "DeployTokens|Your new project deploy token has been created."
msgstr ""
+msgid "Deployed to"
+msgstr ""
+
+msgid "Deploying to"
+msgstr ""
+
msgid "Deprioritize label"
msgstr ""
@@ -2456,6 +2513,9 @@ msgstr ""
msgid "Edit application"
msgstr ""
+msgid "Edit environment"
+msgstr ""
+
msgid "Edit files in the editor and commit changes here"
msgstr ""
@@ -2534,6 +2594,9 @@ msgstr ""
msgid "Environments"
msgstr ""
+msgid "Environments allow you to track deployments of your application %{link_to_read_more}."
+msgstr ""
+
msgid "Environments|An error occurred while fetching the environments."
msgstr ""
@@ -2603,6 +2666,9 @@ msgstr ""
msgid "Environments|Stop environment"
msgstr ""
+msgid "Environments|Stopping"
+msgstr ""
+
msgid "Environments|Updated"
msgstr ""
@@ -2717,6 +2783,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 ""
@@ -2735,6 +2804,9 @@ msgstr ""
msgid "Explore public groups"
msgstr ""
+msgid "External URL"
+msgstr ""
+
msgid "Facebook"
msgstr ""
@@ -2750,6 +2822,9 @@ msgstr ""
msgid "Failed to check related branches."
msgstr ""
+msgid "Failed to deploy to"
+msgstr ""
+
msgid "Failed to load emoji list."
msgstr ""
@@ -3002,9 +3077,15 @@ msgstr ""
msgid "Group Runners"
msgstr ""
+msgid "Group URL"
+msgstr ""
+
msgid "Group avatar"
msgstr ""
+msgid "Group description"
+msgstr ""
+
msgid "Group description (optional)"
msgstr ""
@@ -3537,6 +3618,9 @@ msgstr ""
msgid "Last edited by %{name}"
msgstr ""
+msgid "Last reply by"
+msgstr ""
+
msgid "Last update"
msgstr ""
@@ -3812,6 +3896,9 @@ msgstr ""
msgid "Metrics and profiling"
msgstr ""
+msgid "Metrics for environment"
+msgstr ""
+
msgid "Metrics|Check out the CI/CD documentation on deploying to an environment"
msgstr ""
@@ -3956,6 +4043,9 @@ msgstr ""
msgid "New Application"
msgstr ""
+msgid "New Environment"
+msgstr ""
+
msgid "New Group"
msgstr ""
@@ -3988,6 +4078,9 @@ msgstr ""
msgid "New directory"
msgstr ""
+msgid "New environment"
+msgstr ""
+
msgid "New file"
msgstr ""
@@ -4030,9 +4123,6 @@ msgstr ""
msgid "No"
msgstr ""
-msgid "No Assignee"
-msgstr ""
-
msgid "No Label"
msgstr ""
@@ -4141,12 +4231,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 ""
@@ -4362,6 +4458,9 @@ msgstr ""
msgid "Personal Access Token"
msgstr ""
+msgid "Pick a name"
+msgstr ""
+
msgid "Pipeline"
msgstr ""
@@ -4479,15 +4578,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 ""
@@ -4497,6 +4605,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 ""
@@ -4533,12 +4647,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 ""
@@ -4551,6 +4671,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 ""
@@ -4602,9 +4725,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 ""
@@ -4665,6 +4785,12 @@ msgstr ""
msgid "Profiles|Invalid username"
msgstr ""
+msgid "Profiles|Learn more"
+msgstr ""
+
+msgid "Profiles|Made a private contribution"
+msgstr ""
+
msgid "Profiles|Main settings"
msgstr ""
@@ -4704,6 +4830,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 ""
@@ -4728,6 +4857,9 @@ msgstr ""
msgid "Profiles|Upload new avatar"
msgstr ""
+msgid "Profiles|Use a private email - %{email}"
+msgstr ""
+
msgid "Profiles|Username change failed - %{message}"
msgstr ""
@@ -4890,6 +5022,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 ""
@@ -5022,6 +5157,9 @@ msgstr ""
msgid "Read more"
msgstr ""
+msgid "Read more about environments"
+msgstr ""
+
msgid "Read more about project permissions <strong>%{link_to_help}</strong>"
msgstr ""
@@ -5048,6 +5186,9 @@ msgstr ""
msgid "Register / Sign In"
msgstr ""
+msgid "Register U2F device"
+msgstr ""
+
msgid "Register and see your runners for this group."
msgstr ""
@@ -5117,6 +5258,9 @@ msgstr ""
msgid "Reply to this email directly or %{view_it_on_gitlab}."
msgstr ""
+msgid "Report abuse to GitLab"
+msgstr ""
+
msgid "Reporting"
msgstr ""
@@ -5392,6 +5536,9 @@ msgstr ""
msgid "Secret"
msgstr ""
+msgid "See metrics"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -5485,6 +5632,9 @@ msgstr ""
msgid "Set up a specific Runner automatically"
msgstr ""
+msgid "Set up new U2F device"
+msgstr ""
+
msgid "Set up your project to automatically push and/or pull changes to/from another repository. Branches, tags, and commits will be synced automatically."
msgstr ""
@@ -5565,6 +5715,9 @@ msgstr ""
msgid "Sign in / Register"
msgstr ""
+msgid "Sign in via 2FA code"
+msgstr ""
+
msgid "Sign out"
msgstr ""
@@ -5607,6 +5760,9 @@ msgstr ""
msgid "Something went wrong while fetching comments. Please try again."
msgstr ""
+msgid "Something went wrong while fetching the environments for this merge request. Please try again."
+msgstr ""
+
msgid "Something went wrong while fetching the projects."
msgstr ""
@@ -5784,6 +5940,12 @@ msgstr ""
msgid "Start a %{new_merge_request} with these changes"
msgstr ""
+msgid "Start and due date"
+msgstr ""
+
+msgid "Start date"
+msgstr ""
+
msgid "Start the Runner!"
msgstr ""
@@ -5796,6 +5958,9 @@ msgstr ""
msgid "Status"
msgstr ""
+msgid "Stop environment"
+msgstr ""
+
msgid "Stop impersonation"
msgstr ""
@@ -5805,6 +5970,9 @@ msgstr ""
msgid "Stopped"
msgstr ""
+msgid "Stopping this environment is currently not possible as a deployment is in progress"
+msgstr ""
+
msgid "Storage"
msgstr ""
@@ -5835,9 +6003,6 @@ msgstr ""
msgid "Subscribed"
msgstr ""
-msgid "Summary of issues, merge requests, push events, and comments (Timezone: %{utcFormatted})"
-msgstr ""
-
msgid "Switch branch/tag"
msgstr ""
@@ -5951,6 +6116,9 @@ msgstr ""
msgid "Templates"
msgstr ""
+msgid "Terminal for environment"
+msgstr ""
+
msgid "Terms of Service Agreement and Privacy Policy"
msgstr ""
@@ -6131,7 +6299,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."
@@ -6173,6 +6341,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 ""
@@ -6236,6 +6407,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 ""
@@ -6260,6 +6434,9 @@ msgstr ""
msgid "Time between merge request creation and merge/close"
msgstr ""
+msgid "Time estimate"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -6474,6 +6651,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 ""
@@ -6507,6 +6687,9 @@ msgstr ""
msgid "Token"
msgstr ""
+msgid "Tomorrow"
+msgstr ""
+
msgid "Too many changes to show."
msgstr ""
@@ -6540,6 +6723,12 @@ msgstr ""
msgid "Try again"
msgstr ""
+msgid "Try again?"
+msgstr ""
+
+msgid "Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now."
+msgstr ""
+
msgid "Twitter"
msgstr ""
@@ -6696,9 +6885,6 @@ msgstr ""
msgid "UserProfile|Personal projects"
msgstr ""
-msgid "UserProfile|Recent contributions"
-msgstr ""
-
msgid "UserProfile|Report abuse"
msgstr ""
@@ -6744,6 +6930,9 @@ msgstr ""
msgid "View app"
msgstr ""
+msgid "View deployment"
+msgstr ""
+
msgid "View file @ "
msgstr ""
@@ -6771,6 +6960,9 @@ msgstr ""
msgid "View replaced file @ "
msgstr ""
+msgid "View the documentation"
+msgstr ""
+
msgid "Visibility and access controls"
msgstr ""
@@ -6804,6 +6996,9 @@ msgstr ""
msgid "We don't have enough data to show this stage."
msgstr ""
+msgid "We heard back from your U2F device. You have been authenticated."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
@@ -6822,6 +7017,9 @@ msgstr ""
msgid "Who can see this group?"
msgstr ""
+msgid "Who will be able to see this group?"
+msgstr ""
+
msgid "Wiki"
msgstr ""
@@ -6969,6 +7167,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 ""
@@ -6996,9 +7197,6 @@ msgstr ""
msgid "You can also star a label to make it a priority label."
msgstr ""
-msgid "You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}"
-msgstr ""
-
msgid "You can easily contribute to them by requesting to join these groups."
msgstr ""
@@ -7020,6 +7218,9 @@ msgstr ""
msgid "You can set up jobs to only use Runners with specific tags. Separate tags with commas."
msgstr ""
+msgid "You can test your .gitlab-ci.yml in %{linkStart}CI Lint%{linkEnd}."
+msgstr ""
+
msgid "You cannot write to this read-only GitLab instance."
msgstr ""
@@ -7032,6 +7233,9 @@ msgstr ""
msgid "You don't have any authorized applications"
msgstr ""
+msgid "You don't have any deployments right now."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -7047,6 +7251,9 @@ msgstr ""
msgid "You need permission."
msgstr ""
+msgid "You need to register a two-factor authentication app before you can set up a U2F device."
+msgstr ""
+
msgid "You will loose all changes you've made to this file. This action cannot be undone."
msgstr ""
@@ -7107,12 +7314,18 @@ msgstr ""
msgid "Your Todos"
msgstr ""
+msgid "Your U2F device needs to be set up. Plug it in (if not already) and click the button on the left."
+msgstr ""
+
msgid "Your applications (%{size})"
msgstr ""
msgid "Your authorized applications"
msgstr ""
+msgid "Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer)."
+msgstr ""
+
msgid "Your changes can be committed to %{branch_name} because a merge request is open."
msgstr ""
@@ -7122,6 +7335,9 @@ msgstr ""
msgid "Your comment will not be visible to the public."
msgstr ""
+msgid "Your device was successfully set up! Give it a name and register it with the GitLab server."
+msgstr ""
+
msgid "Your groups"
msgstr ""
@@ -7175,6 +7391,9 @@ msgstr ""
msgid "enabled"
msgstr ""
+msgid "error code:"
+msgstr ""
+
msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
msgstr ""
@@ -7436,6 +7655,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..5e2dbce1104 100644
--- a/locale/gl_ES/gitlab.po
+++ b/locale/gl_ES/gitlab.po
@@ -561,7 +561,7 @@ msgstr ""
msgid "An error occured whilst fetching the job trace."
msgstr ""
-msgid "An error occured whilst fetching the latest pipline."
+msgid "An error occured whilst fetching the latest pipeline."
msgstr ""
msgid "An error occured whilst loading all the files."
@@ -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..9aadf885770 100644
--- a/locale/he_IL/gitlab.po
+++ b/locale/he_IL/gitlab.po
@@ -621,7 +621,7 @@ msgstr ""
msgid "An error occured whilst fetching the job trace."
msgstr ""
-msgid "An error occured whilst fetching the latest pipline."
+msgid "An error occured whilst fetching the latest pipeline."
msgstr ""
msgid "An error occured whilst loading all the files."
@@ -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..641886e65b0 100644
--- a/locale/id_ID/gitlab.po
+++ b/locale/id_ID/gitlab.po
@@ -531,7 +531,7 @@ msgstr ""
msgid "An error occured whilst fetching the job trace."
msgstr ""
-msgid "An error occured whilst fetching the latest pipline."
+msgid "An error occured whilst fetching the latest pipeline."
msgstr ""
msgid "An error occured whilst loading all the files."
@@ -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..3b43d563dc5 100644
--- a/locale/it/gitlab.po
+++ b/locale/it/gitlab.po
@@ -561,7 +561,7 @@ msgstr ""
msgid "An error occured whilst fetching the job trace."
msgstr ""
-msgid "An error occured whilst fetching the latest pipline."
+msgid "An error occured whilst fetching the latest pipeline."
msgstr ""
msgid "An error occured whilst loading all the files."
@@ -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..a254bfba027 100644
--- a/locale/ja/gitlab.po
+++ b/locale/ja/gitlab.po
@@ -531,7 +531,7 @@ msgstr "新しいブランチの作成中にエラーが発生しました。"
msgid "An error occured whilst fetching the job trace."
msgstr "ジョブトレースの取得中にエラーが発生しました。"
-msgid "An error occured whilst fetching the latest pipline."
+msgid "An error occured whilst fetching the latest pipeline."
msgstr "最新のパイプラインの取得中にエラーが発生しました。"
msgid "An error occured whilst loading all the files."
@@ -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..daced0494cb 100644
--- a/locale/ko/gitlab.po
+++ b/locale/ko/gitlab.po
@@ -531,7 +531,7 @@ msgstr "새 브랜치를 만드는 동안 오류가 발생했습니다."
msgid "An error occured whilst fetching the job trace."
msgstr ""
-msgid "An error occured whilst fetching the latest pipline."
+msgid "An error occured whilst fetching the latest pipeline."
msgstr ""
msgid "An error occured whilst loading all the files."
@@ -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..cf2c7224171 100644
--- a/locale/mn_MN/gitlab.po
+++ b/locale/mn_MN/gitlab.po
@@ -561,7 +561,7 @@ msgstr ""
msgid "An error occured whilst fetching the job trace."
msgstr ""
-msgid "An error occured whilst fetching the latest pipline."
+msgid "An error occured whilst fetching the latest pipeline."
msgstr ""
msgid "An error occured whilst loading all the files."
@@ -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..df490378e9c 100644
--- a/locale/nb_NO/gitlab.po
+++ b/locale/nb_NO/gitlab.po
@@ -561,7 +561,7 @@ msgstr ""
msgid "An error occured whilst fetching the job trace."
msgstr ""
-msgid "An error occured whilst fetching the latest pipline."
+msgid "An error occured whilst fetching the latest pipeline."
msgstr ""
msgid "An error occured whilst loading all the files."
@@ -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..8f9c3161a26 100644
--- a/locale/nl_NL/gitlab.po
+++ b/locale/nl_NL/gitlab.po
@@ -561,7 +561,7 @@ msgstr ""
msgid "An error occured whilst fetching the job trace."
msgstr ""
-msgid "An error occured whilst fetching the latest pipline."
+msgid "An error occured whilst fetching the latest pipeline."
msgstr ""
msgid "An error occured whilst loading all the files."
@@ -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..ebe5ee77d71 100644
--- a/locale/pl_PL/gitlab.po
+++ b/locale/pl_PL/gitlab.po
@@ -621,7 +621,7 @@ msgstr ""
msgid "An error occured whilst fetching the job trace."
msgstr ""
-msgid "An error occured whilst fetching the latest pipline."
+msgid "An error occured whilst fetching the latest pipeline."
msgstr ""
msgid "An error occured whilst loading all the files."
@@ -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..39aa1256e4b 100644
--- a/locale/pt_BR/gitlab.po
+++ b/locale/pt_BR/gitlab.po
@@ -561,7 +561,7 @@ msgstr "Um erro ocorreu ao criar o novo branch."
msgid "An error occured whilst fetching the job trace."
msgstr "Ocorreu um erro ao carregar o rastro da tarefa."
-msgid "An error occured whilst fetching the latest pipline."
+msgid "An error occured whilst fetching the latest pipeline."
msgstr "Ocorreu um erro ao carregar o último pipeline."
msgid "An error occured whilst loading all the files."
@@ -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..a6b70d486fb 100644
--- a/locale/ro_RO/gitlab.po
+++ b/locale/ro_RO/gitlab.po
@@ -591,7 +591,7 @@ msgstr ""
msgid "An error occured whilst fetching the job trace."
msgstr ""
-msgid "An error occured whilst fetching the latest pipline."
+msgid "An error occured whilst fetching the latest pipeline."
msgstr ""
msgid "An error occured whilst loading all the files."
@@ -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..739f96cba9a 100644
--- a/locale/ru/gitlab.po
+++ b/locale/ru/gitlab.po
@@ -621,7 +621,7 @@ msgstr ""
msgid "An error occured whilst fetching the job trace."
msgstr ""
-msgid "An error occured whilst fetching the latest pipline."
+msgid "An error occured whilst fetching the latest pipeline."
msgstr ""
msgid "An error occured whilst loading all the files."
@@ -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..75df8a29e6b 100644
--- a/locale/sq_AL/gitlab.po
+++ b/locale/sq_AL/gitlab.po
@@ -561,7 +561,7 @@ msgstr ""
msgid "An error occured whilst fetching the job trace."
msgstr ""
-msgid "An error occured whilst fetching the latest pipline."
+msgid "An error occured whilst fetching the latest pipeline."
msgstr ""
msgid "An error occured whilst loading all the files."
@@ -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..1a8d2faf307 100644
--- a/locale/tr_TR/gitlab.po
+++ b/locale/tr_TR/gitlab.po
@@ -561,7 +561,7 @@ msgstr ""
msgid "An error occured whilst fetching the job trace."
msgstr ""
-msgid "An error occured whilst fetching the latest pipline."
+msgid "An error occured whilst fetching the latest pipeline."
msgstr ""
msgid "An error occured whilst loading all the files."
@@ -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..30f1b5769d6 100644
--- a/locale/uk/gitlab.po
+++ b/locale/uk/gitlab.po
@@ -621,7 +621,7 @@ msgstr "Помилка при створенні нової гілки."
msgid "An error occured whilst fetching the job trace."
msgstr "Трапилася помилка при отриманні логу завдання."
-msgid "An error occured whilst fetching the latest pipline."
+msgid "An error occured whilst fetching the latest pipeline."
msgstr "Трапилася помилка при отриманні даних останнього конвеєра."
msgid "An error occured whilst loading all the files."
@@ -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..19fa17eaff1 100644
--- a/locale/zh_CN/gitlab.po
+++ b/locale/zh_CN/gitlab.po
@@ -531,7 +531,7 @@ msgstr "创建分支时发生错误。"
msgid "An error occured whilst fetching the job trace."
msgstr "获取作业日志时发生错误。"
-msgid "An error occured whilst fetching the latest pipline."
+msgid "An error occured whilst fetching the latest pipeline."
msgstr "获取流水线时发生错误。"
msgid "An error occured whilst loading all the files."
@@ -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..6987f307dbb 100644
--- a/locale/zh_HK/gitlab.po
+++ b/locale/zh_HK/gitlab.po
@@ -531,7 +531,7 @@ msgstr ""
msgid "An error occured whilst fetching the job trace."
msgstr ""
-msgid "An error occured whilst fetching the latest pipline."
+msgid "An error occured whilst fetching the latest pipeline."
msgstr ""
msgid "An error occured whilst loading all the files."
@@ -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..4e0b07e717f 100644
--- a/locale/zh_TW/gitlab.po
+++ b/locale/zh_TW/gitlab.po
@@ -531,7 +531,7 @@ msgstr "創建新分支時發生錯誤。"
msgid "An error occured whilst fetching the job trace."
msgstr "取得工作追蹤資訊時發生錯誤"
-msgid "An error occured whilst fetching the latest pipline."
+msgid "An error occured whilst fetching the latest pipeline."
msgstr "取得最新流水線時發生錯誤"
msgid "An error occured whilst loading all the files."
@@ -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 086617dc265..ac4b4672c86 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.8.0",
+ "@gitlab/svgs": "^1.38.0",
+ "@gitlab/ui": "^1.11.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 36a37dbb270..c0d5244dbfa 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -36,44 +36,40 @@ module QA
##
# GitLab QA fabrication mechanisms
#
- module Factory
- autoload :ApiFabricator, 'qa/factory/api_fabricator'
- autoload :Base, 'qa/factory/base'
- autoload :Dependency, 'qa/factory/dependency'
- 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 :SecretVariable, 'qa/factory/resource/secret_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
@@ -100,7 +96,8 @@ module QA
module Integration
autoload :Github, 'qa/scenario/test/integration/github'
- autoload :LDAP, 'qa/scenario/test/integration/ldap'
+ autoload :LDAPNoTLS, 'qa/scenario/test/integration/ldap_no_tls'
+ autoload :LDAPTLS, 'qa/scenario/test/integration/ldap_tls'
autoload :InstanceSAML, 'qa/scenario/test/integration/instance_saml'
autoload :Kubernetes, 'qa/scenario/test/integration/kubernetes'
autoload :Mattermost, 'qa/scenario/test/integration/mattermost'
@@ -183,7 +180,7 @@ module QA
autoload :DeployKeys, 'qa/page/project/settings/deploy_keys'
autoload :DeployTokens, 'qa/page/project/settings/deploy_tokens'
autoload :ProtectedBranches, 'qa/page/project/settings/protected_branches'
- autoload :SecretVariables, 'qa/page/project/settings/secret_variables'
+ autoload :CiVariables, 'qa/page/project/settings/ci_variables'
autoload :Runners, 'qa/page/project/settings/runners'
autoload :MergeRequest, 'qa/page/project/settings/merge_request'
autoload :Members, 'qa/page/project/settings/members'
diff --git a/qa/qa/factory/README.md b/qa/qa/factory/README.md
deleted file mode 100644
index 10140e39510..00000000000
--- a/qa/qa/factory/README.md
+++ /dev/null
@@ -1,474 +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, :size
-
- def initialize(name)
- @name = name
- end
-
- 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, :size
-
- def initialize(name)
- @name = name
- end
-
- 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
-
- 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 dependencies
-
-A resource may need an other resource to exist first. For instance, a project
-needs a group to be created in.
-
-To define a dependency, you can use the `dependency` DSL method.
-The first argument is a factory class, then you should pass `as: <name>` to give
-a name to the dependency.
-That will allow access to the dependency 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` dependency to it:
-
-```ruby
-module QA
- module Factory
- module Resource
- class Shirt < Factory::Base
- attr_accessor :name, :size
-
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-to-create-a-shirt'
- end
-
- def initialize(name)
- @name = name
- 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 dependencies are always built via the API fabrication method if
-supported by their factories.**
-
-### Define attributes on the created resource
-
-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.
-
-To define an attribute, you can use the `product` DSL method.
-The first argument is the attribute name, then you should define a name for the
-dependency to be accessible from your resource object's methods.
-
-Let's take the `Shirt` factory, and define a `:brand` attribute:
-
-```ruby
-module QA
- module Factory
- module Resource
- class Shirt < Factory::Base
- attr_accessor :name, :size
-
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-to-create-a-shirt'
- end
-
- # Attribute populated from the Browser UI (using the block)
- product :brand do
- Page::Shirt::Show.perform do |shirt_show|
- shirt_show.fetch_brand_from_page
- end
- end
-
- def initialize(name)
- @name = name
- 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
-```
-
-#### Inherit a factory's attribute
-
-Sometimes, you want a resource to inherit its factory attributes. For instance,
-it could be useful to pass the `size` attribute from the `Shirt` factory to the
-created resource.
-You can do that by defining `product :attribute_name` without a block.
-
-Let's take the `Shirt` factory, and define a `:name` and a `:size` attributes:
-
-```ruby
-module QA
- module Factory
- module Resource
- class Shirt < Factory::Base
- attr_accessor :name, :size
-
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-to-create-a-shirt'
- end
-
- # Attribute from the Browser UI (using the block)
- product :brand do
- Page::Shirt::Show.perform do |shirt_show|
- shirt_show.fetch_brand_from_page
- end
- end
-
- # Attribute inherited from the Shirt factory if present,
- # or a QA::Factory::Product::NoValueError is raised otherwise
- product :name
- product :size
-
- def initialize(name)
- @name = name
- 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
-```
-
-#### 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',
- size: 'extra-small',
- 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.
-
-For both attributes, you will need to define an inherited attribute, as shown
-in "Inherit a factory's attribute" above, but in the case of `main_fabric`, you
-will need to implement the
-`#transform_api_resource` method to first populate the `:main_fabric` key in the
-API response so that it can be used later to automatically populate the
-attribute on your resource.
-
-If an attribute can only be retrieved from the API response, you should define
-a block to give it a default value, otherwise you could get a
-`QA::Factory::Product::NoValueError` when creating your resource via the
-Browser UI.
-
-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
- attr_accessor :name, :size
-
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-to-create-a-shirt'
- end
-
- # Attribute fetched from the API response if present,
- # or from the Browser UI otherwise (using the block)
- product :brand do
- Page::Shirt::Show.perform do |shirt_show|
- shirt_show.fetch_brand_from_page
- end
- end
-
- # Attribute fetched from the API response if present,
- # or from the Shirt factory if present,
- # or a QA::Factory::Product::NoValueError is raised otherwise
- product :name
- product :size
- product :style do
- 'unknown'
- end
- product :main_fabric do
- 'unknown'
- end
-
- def initialize(name)
- @name = name
- 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
-
- private
-
- def transform_api_resource(api_response)
- api_response[:main_fabric] = api_response[:materials][0][0]
- api_response
- end
- end
- end
- end
-end
-```
-
-**Notes on attributes precedence:**
-
-- attributes from the API response take precedence over attributes from the
- Browser UI
-- attributes from the Browser UI take precedence over attributes from the
- factory (i.e inherited)
-- attributes without a value will raise a `QA::Factory::Product::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!('my-shirt') do |shirt|
- shirt.size = 'small'
-end
-
-expect(page).to have_text(my_shirt.brand) # => "a-brand-new-brand" from the API response
-expect(page).to have_text(my_shirt.name) # => "my-shirt" from the inherited factory's attribute
-expect(page).to have_text(my_shirt.size) # => "extra-small" 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 (transformed) API response
-```
-
-If you explicitely 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!('my-shirt') do |shirt|
- shirt.size = 'small'
-end
-
-expect(page).to have_text(my_shirt.brand) # => the brand name fetched from the `Page::Shirt::Show` page
-expect(page).to have_text(my_shirt.name) # => "my-shirt" from the inherited factory's attribute
-expect(page).to have_text(my_shirt.size) # => "small" from the inherited factory's attribute
-expect(page).to have_text(my_shirt.style) # => "unknown" from the attribute block
-expect(page).to have_text(my_shirt.main_fabric) # => "unknown" from the attribute block
-```
-
-You can also explicitely use the API fabrication method, by calling the
-`.fabricate_via_api!` method:
-
-```ruby
-my_shirt = Factory::Resource::Shirt.fabricate_via_api!('my-shirt') do |shirt|
- shirt.size = 'small'
-end
-```
-
-In this case, the result will be similar to calling `Factory::Resource::Shirt.fabricate!('my-shirt')`.
-
-## 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/base.rb b/qa/qa/factory/base.rb
deleted file mode 100644
index e1dc23d350d..00000000000
--- a/qa/qa/factory/base.rb
+++ /dev/null
@@ -1,115 +0,0 @@
-# frozen_string_literal: true
-
-require 'forwardable'
-require 'capybara/dsl'
-
-module QA
- module Factory
- class Base
- extend SingleForwardable
- include ApiFabricator
- extend Capybara::DSL
-
- def_delegators :evaluator, :dependency, :dependencies
- def_delegators :evaluator, :product, :attributes
-
- def fabricate!(*_args)
- raise NotImplementedError
- end
-
- def self.fabricate!(*args, &prepare_block)
- fabricate_via_api!(*args, &prepare_block)
- rescue NotImplementedError
- fabricate_via_browser_ui!(*args, &prepare_block)
- end
-
- def self.fabricate_via_browser_ui!(*args, &prepare_block)
- options = args.extract_options!
- factory = options.fetch(:factory) { 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) }
-
- current_url
- end
- end
-
- def self.fabricate_via_api!(*args, &prepare_block)
- options = args.extract_options!
- factory = options.fetch(:factory) { new }
- parents = options.fetch(:parents) { [] }
-
- raise NotImplementedError unless factory.api_support?
-
- factory.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! }
- end
- end
-
- def self.do_fabricate!(factory:, prepare_block:, parents: [])
- prepare_block.call(factory) if prepare_block
-
- dependencies.each do |signature|
- Factory::Dependency.new(factory, signature).build!(parents: parents + [self])
- end
-
- resource_web_url = yield
-
- Factory::Product.populate!(factory, resource_web_url)
- end
- private_class_method :do_fabricate!
-
- def self.log_fabrication(method, factory, parents, args)
- return yield unless Runtime::Env.debug?
-
- start = Time.now
- prefix = "==#{'=' * parents.size}>"
- msg = [prefix]
- msg << "Built a #{name}"
- msg << "as a dependency of #{parents.last}" if parents.any?
- msg << "via #{method} with args #{args}"
-
- yield.tap do
- msg << "in #{Time.now - start} seconds"
- puts msg.join(' ')
- puts if parents.empty?
- end
- end
- private_class_method :log_fabrication
-
- def self.evaluator
- @evaluator ||= Factory::Base::DSL.new(self)
- end
- private_class_method :evaluator
-
- class DSL
- attr_reader :dependencies, :attributes
-
- def initialize(base)
- @base = base
- @dependencies = []
- @attributes = []
- end
-
- def dependency(factory, as:, &block)
- as.tap do |name|
- @base.class_eval { attr_accessor name }
-
- Dependency::Signature.new(name, factory, block).tap do |signature|
- @dependencies << signature
- end
- end
- end
-
- def product(attribute, &block)
- Product::Attribute.new(attribute, block).tap do |signature|
- @attributes << signature
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/dependency.rb b/qa/qa/factory/dependency.rb
deleted file mode 100644
index 655e2677db0..00000000000
--- a/qa/qa/factory/dependency.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-module QA
- module Factory
- class Dependency
- Signature = Struct.new(:name, :factory, :block)
-
- def initialize(caller_factory, dependency_signature)
- @caller_factory = caller_factory
- @dependency_signature = dependency_signature
- end
-
- def overridden?
- !!@caller_factory.public_send(@dependency_signature.name)
- end
-
- def build!(parents: [])
- return if overridden?
-
- dependency = @dependency_signature.factory.fabricate!(parents: parents) do |factory|
- @dependency_signature.block&.call(factory, @caller_factory)
- end
-
- dependency.tap do |dependency|
- @caller_factory.public_send("#{@dependency_signature.name}=", dependency)
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/product.rb b/qa/qa/factory/product.rb
deleted file mode 100644
index 17fe908eaa2..00000000000
--- a/qa/qa/factory/product.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-require 'capybara/dsl'
-
-module QA
- module Factory
- class Product
- include Capybara::DSL
-
- NoValueError = Class.new(RuntimeError)
-
- attr_reader :factory, :web_url
-
- Attribute = Struct.new(:name, :block)
-
- def initialize(factory, web_url)
- @factory = factory
- @web_url = web_url
-
- populate_attributes!
- end
-
- def visit!
- visit(web_url)
- end
-
- def self.populate!(factory, web_url)
- new(factory, web_url)
- end
-
- private
-
- def populate_attributes!
- factory.class.attributes.each do |attribute|
- instance_exec(factory, attribute.block) do |factory, block|
- value = attribute_value(attribute, block)
-
- raise NoValueError, "No value was computed for product #{attribute.name} of factory #{factory.class.name}." unless value
-
- define_singleton_method(attribute.name) { value }
- end
- end
- end
-
- def attribute_value(attribute, block)
- factory.api_resource&.dig(attribute.name) ||
- (block && block.call(factory)) ||
- (factory.respond_to?(attribute.name) && factory.public_send(attribute.name))
- 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 f3b52565d17..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
-
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'protected-branch-project'
- 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.act do
- click_repository_settings
- end
-
- 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/deploy_key.rb b/qa/qa/factory/resource/deploy_key.rb
deleted file mode 100644
index 4c53c500c27..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
-
- product :fingerprint do |resource|
- Page::Project::Settings::Repository.act do
- expand_deploy_keys do |key|
- key_offset = key.key_titles.index do |title|
- title.text == resource.title
- end
-
- key.key_fingerprints[key_offset].text
- end
- end
- end
-
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-to-deploy'
- project.description = 'project for adding deploy key test'
- end
-
- def fabricate!
- project.visit!
-
- Page::Project::Menu.act do
- click_repository_settings
- end
-
- 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 159f79ac50b..00000000000
--- a/qa/qa/factory/resource/deploy_token.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-module QA
- module Factory
- module Resource
- class DeployToken < Factory::Base
- attr_accessor :name, :expires_at
-
- product :username do |resource|
- Page::Project::Settings::Repository.act do
- expand_deploy_tokens do |token|
- token.token_username
- end
- end
- end
-
- product :password do |password|
- Page::Project::Settings::Repository.act do
- expand_deploy_tokens do |token|
- token.token_password
- end
- end
- end
-
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-to-deploy'
- project.description = 'project for adding deploy token test'
- 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 f8dea06d361..00000000000
--- a/qa/qa/factory/resource/file.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-module QA
- module Factory
- module Resource
- class File < Factory::Base
- attr_accessor :name,
- :content,
- :commit_message
-
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-with-new-file'
- 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.act { 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 6e2a668df64..00000000000
--- a/qa/qa/factory/resource/fork.rb
+++ /dev/null
@@ -1,64 +0,0 @@
-module QA
- module Factory
- module Resource
- class Fork < Factory::Base
- dependency Factory::Repository::ProjectPush, as: :push
-
- dependency Factory::Resource::User, as: :user do |user|
- if Runtime::Env.forker?
- user.username = Runtime::Env.forker_username
- user.password = Runtime::Env.forker_password
- end
- end
-
- product :user
-
- 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!
- visit_project_with_retry
-
- Page::Project::Show.act { fork_project }
-
- Page::Project::Fork::New.perform do |fork_new|
- fork_new.choose_namespace(user.name)
- end
-
- Page::Layout::Banner.act { has_notice?('The project was successfully forked.') }
- 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 2688328df92..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
-
- dependency Factory::Resource::Sandbox, as: :sandbox
-
- product :id do
- true # We don't retrieve the Group ID when using the Browser UI
- end
-
- 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 9b444cb0bf1..00000000000
--- a/qa/qa/factory/resource/issue.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-module QA
- module Factory
- module Resource
- class Issue < Factory::Base
- attr_accessor :title, :description, :project
-
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-for-issues'
- project.description = 'project for adding issues'
- end
-
- product :project
- product :title
-
- def fabricate!
- project.visit!
-
- Page::Project::Show.act do
- go_to_new_issue
- end
-
- 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 cdee35c54e3..00000000000
--- a/qa/qa/factory/resource/kubernetes_cluster.rb
+++ /dev/null
@@ -1,60 +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
-
- product :ingress_ip do
- Page::Project::Operations::Kubernetes::Show.perform do |page|
- page.ingress_ip
- end
- end
-
- def fabricate!
- @project.visit!
-
- Page::Project::Menu.act { click_operations_kubernetes }
-
- Page::Project::Operations::Kubernetes::Index.perform do |page|
- page.add_kubernetes_cluster
- end
-
- Page::Project::Operations::Kubernetes::Add.perform do |page|
- page.add_existing_cluster
- end
-
- 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 4080f15bf66..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 :title,
- :description,
- :color
-
- product(:title) { |factory| factory.title }
-
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-with-label'
- 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.act { go_to_labels }
- Page::Label::Index.act { 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 d30da8a3db0..00000000000
--- a/qa/qa/factory/resource/merge_request.rb
+++ /dev/null
@@ -1,65 +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
-
- product :project
- product :source_branch
-
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-with-merge-request'
- end
-
- dependency Factory::Repository::ProjectPush, as: :target do |push, factory|
- factory.project.visit!
- push.project = factory.project
- push.branch_name = 'master'
- push.remote_branch = factory.target_branch
- end
-
- dependency Factory::Repository::ProjectPush, as: :source do |push, factory|
- push.project = factory.project
- push.branch_name = factory.target_branch
- push.remote_branch = factory.source_branch
- push.new_branch = false
- push.file_name = "added_file.txt"
- push.file_content = "File Added"
- 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!
- project.visit!
- Page::Project::Show.act { 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 6caaf65f673..00000000000
--- a/qa/qa/factory/resource/merge_request_from_fork.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-module QA
- module Factory
- module Resource
- class MergeRequestFromFork < MergeRequest
- attr_accessor :fork_branch
-
- dependency Factory::Resource::Fork, as: :fork
-
- dependency Factory::Repository::ProjectPush, as: :push do |push, factory|
- push.project = factory.fork
- push.branch_name = factory.fork_branch
- push.file_name = 'file2.txt'
- push.user = factory.fork.user
- end
-
- def fabricate!
- fork.visit!
- Page::Project::Show.act { new_merge_request }
- Page::MergeRequest::New.act { 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 166054cfcdc..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
-
- product :access_token do
- Page::Profile::PersonalAccessTokens.act { created_access_token }
- end
-
- def fabricate!
- Page::Main::Menu.act { go_to_profile_settings }
- Page::Profile::Menu.act { 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 105e42b23ec..00000000000
--- a/qa/qa/factory/resource/project.rb
+++ /dev/null
@@ -1,79 +0,0 @@
-require 'securerandom'
-
-module QA
- module Factory
- module Resource
- class Project < Factory::Base
- attr_accessor :description
- attr_reader :name
-
- dependency Factory::Resource::Group, as: :group
-
- product :group
- product :name
-
- product :repository_ssh_location do
- Page::Project::Show.act do
- choose_repository_clone_ssh
- repository_location
- end
- end
-
- product :repository_http_location do
- Page::Project::Show.act do
- choose_repository_clone_http
- 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.act { 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 a45e7fee03b..00000000000
--- a/qa/qa/factory/resource/project_imported_from_github.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-require 'securerandom'
-
-module QA
- module Factory
- module Resource
- class ProjectImportedFromGithub < Resource::Project
- attr_writer :personal_access_token, :github_repository_path
-
- dependency Factory::Resource::Group, as: :group
-
- product :name
-
- def fabricate!
- group.visit!
-
- Page::Group::Show.act { 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 35383842142..00000000000
--- a/qa/qa/factory/resource/project_milestone.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-module QA
- module Factory
- module Resource
- class ProjectMilestone < Factory::Base
- attr_accessor :description
- attr_reader :title
-
- dependency Factory::Resource::Project, as: :project
-
- product :title
-
- def title=(title)
- @title = "#{title}-#{SecureRandom.hex(4)}"
- @description = 'A milestone'
- end
-
- def fabricate!
- project.visit!
-
- Page::Project::Menu.act do
- click_issues
- click_milestones
- end
-
- Page::Project::Milestone::Index.act { 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 7ac65fe6913..00000000000
--- a/qa/qa/factory/resource/runner.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-require 'securerandom'
-
-module QA
- module Factory
- module Resource
- class Runner < Factory::Base
- attr_writer :name, :tags, :image
-
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-with-ci-cd'
- project.description = 'Project with CI/CD Pipelines'
- 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.act { 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 e592f4e0dd2..00000000000
--- a/qa/qa/factory/resource/sandbox.rb
+++ /dev/null
@@ -1,63 +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
-
- product :id do
- true # We don't retrieve the Group ID when using the Browser UI
- end
- product :path
-
- def initialize
- @path = Runtime::Namespace.sandbox_name
- end
-
- def fabricate!
- Page::Main::Menu.act { 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/secret_variable.rb b/qa/qa/factory/resource/secret_variable.rb
deleted file mode 100644
index 4084a7fc2cd..00000000000
--- a/qa/qa/factory/resource/secret_variable.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-module QA
- module Factory
- module Resource
- class SecretVariable < Factory::Base
- attr_accessor :key, :value
-
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-with-secret-variables'
- project.description = 'project for adding secret variable test'
- end
-
- def fabricate!
- project.visit!
-
- Page::Project::Menu.act { click_ci_cd_settings }
-
- Page::Project::Settings::CICD.perform do |setting|
- setting.expand_secret_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/ssh_key.rb b/qa/qa/factory/resource/ssh_key.rb
deleted file mode 100644
index a512d071dd4..00000000000
--- a/qa/qa/factory/resource/ssh_key.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-module QA
- module Factory
- module Resource
- class SSHKey < Factory::Base
- extend Forwardable
-
- attr_accessor :title
- attr_reader :private_key, :public_key, :fingerprint
- def_delegators :key, :private_key, :public_key, :fingerprint
-
- product :private_key
- product :title
- product :fingerprint
-
- def key
- @key ||= Runtime::Key::RSA.new
- end
-
- def fabricate!
- Page::Main::Menu.act { go_to_profile_settings }
- Page::Profile::Menu.act { 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 36edf787b64..00000000000
--- a/qa/qa/factory/resource/user.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-require 'securerandom'
-
-module QA
- module Factory
- module Resource
- class User < Factory::Base
- attr_reader :unique_id
- attr_writer :username, :password, :name, :email
-
- 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
-
- product :name
- product :username
- product :email
- product :password
-
- def fabricate!
- # Don't try to log-out if we're not logged-in
- if Page::Main::Menu.act { 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 d697433736e..00000000000
--- a/qa/qa/factory/resource/wiki.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-module QA
- module Factory
- module Resource
- class Wiki < Factory::Base
- attr_accessor :title, :content, :message
-
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-for-wikis'
- project.description = 'project for adding wikis'
- 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 c6a8891d398..7f959441dac 100644
--- a/qa/qa/git/repository.rb
+++ b/qa/qa/git/repository.rb
@@ -109,25 +109,43 @@ module QA
known_hosts_file.close(true)
end
- private
+ 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
- attr_reader :uri, :username, :password, :known_hosts_file, :private_key_file
+ 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 debug?
- Runtime::Env.respond_to?(:verbose?) && Runtime::Env.verbose?
+ 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
+
def ssh_key_set?
!private_key_file.nil?
end
- def run(command_str)
- command = [env_vars, command_str, '2>&1'].compact.join(' ')
- warn "DEBUG: command=[#{command}]" if debug?
+ 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)
output = output.chomp.gsub(/\s+$/, '')
- warn "DEBUG: output=[#{output}]" if debug?
+ Runtime::Logger.debug "Git: output=[#{output}]"
output
end
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/profile/personal_access_tokens.rb b/qa/qa/page/profile/personal_access_tokens.rb
index 2f0202951bb..9191dbe9cf3 100644
--- a/qa/qa/page/profile/personal_access_tokens.rb
+++ b/qa/qa/page/profile/personal_access_tokens.rb
@@ -8,7 +8,7 @@ module QA
element :scopes_api_radios, "label :scopes" # rubocop:disable QA/ElementWithPattern
end
- view 'app/views/profiles/personal_access_tokens/index.html.haml' do
+ view 'app/views/shared/_personal_access_tokens_created_container.html.haml' do
element :create_token_field, "text_field_tag 'created-personal-access-token'" # rubocop:disable QA/ElementWithPattern
end
diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb
index 1062f0b2dbb..23def93c7dd 100644
--- a/qa/qa/page/project/issue/show.rb
+++ b/qa/qa/page/project/issue/show.rb
@@ -7,35 +7,47 @@ module QA
class Show < Page::Base
include Page::Component::Issuable::Common
- view 'app/views/projects/issues/show.html.haml' do
- element :issue_details, '.issue-details' # rubocop:disable QA/ElementWithPattern
- element :title, '.title' # rubocop:disable QA/ElementWithPattern
- end
-
view 'app/views/shared/notes/_form.html.haml' do
element :new_note_form, 'new-note' # rubocop:disable QA/ElementWithPattern
element :new_note_form, 'attr: :note' # rubocop:disable QA/ElementWithPattern
end
- view 'app/views/shared/notes/_comment_button.html.haml' do
- element :comment_button, '%strong Comment' # rubocop:disable QA/ElementWithPattern
+ view 'app/assets/javascripts/notes/components/comment_form.vue' do
+ element :comment_button
+ element :comment_input
end
- def issue_title
- find('.issue-details .title').text
+ view 'app/assets/javascripts/notes/components/discussion_filter.vue' do
+ element :discussion_filter
+ element :filter_options
end
# Adds a comment to an issue
# attachment option should be an absolute path
def comment(text, attachment: nil)
- fill_in(with: text, name: 'note[note]')
+ fill_element :comment_input, text
unless attachment.nil?
QA::Page::Component::Dropzone.new(self, '.new-note')
.attach_file(attachment)
end
- click_on 'Comment'
+ click_element :comment_button
+ end
+
+ 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
+
+ def select_all_activities_filter
+ click_element :discussion_filter
+ all_elements(:filter_options).first.click
end
end
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/page/project/settings/ci_cd.rb b/qa/qa/page/project/settings/ci_cd.rb
index cc5fc370a5a..12c2409a5a7 100644
--- a/qa/qa/page/project/settings/ci_cd.rb
+++ b/qa/qa/page/project/settings/ci_cd.rb
@@ -25,9 +25,9 @@ module QA # rubocop:disable Naming/FileName
end
end
- def expand_secret_variables(&block)
+ def expand_ci_variables(&block)
expand_section(:variables_settings) do
- Settings::SecretVariables.perform(&block)
+ Settings::CiVariables.perform(&block)
end
end
diff --git a/qa/qa/page/project/settings/secret_variables.rb b/qa/qa/page/project/settings/ci_variables.rb
index 6a87ef472e4..e7a6e4bf628 100644
--- a/qa/qa/page/project/settings/secret_variables.rb
+++ b/qa/qa/page/project/settings/ci_variables.rb
@@ -2,7 +2,7 @@ module QA
module Page
module Project
module Settings
- class SecretVariables < Page::Base
+ class CiVariables < Page::Base
include Common
view 'app/views/ci/variables/_variable_row.html.haml' do
@@ -12,7 +12,7 @@ module QA
end
view 'app/views/ci/variables/_index.html.haml' do
- element :save_variables, '.js-secret-variables-save-button' # rubocop:disable QA/ElementWithPattern
+ element :save_variables, '.js-ci-variables-save-button' # rubocop:disable QA/ElementWithPattern
element :reveal_values, '.js-secret-value-reveal-button' # rubocop:disable QA/ElementWithPattern
end
@@ -33,7 +33,7 @@ module QA
end
def save_variables
- find('.js-secret-variables-save-button').click
+ find('.js-ci-variables-save-button').click
end
def reveal_variables
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/resource/base.rb b/qa/qa/resource/base.rb
new file mode 100644
index 00000000000..f3eefb70520
--- /dev/null
+++ b/qa/qa/resource/base.rb
@@ -0,0 +1,154 @@
+# frozen_string_literal: true
+
+require 'forwardable'
+require 'capybara/dsl'
+
+module QA
+ module Resource
+ class Base
+ extend SingleForwardable
+ include ApiFabricator
+ extend Capybara::DSL
+
+ NoValueError = Class.new(RuntimeError)
+
+ def_delegators :evaluator, :attribute
+
+ def fabricate!(*_args)
+ raise NotImplementedError
+ end
+
+ def visit!
+ 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 #{name} of #{self.class.name}." unless value
+
+ value
+ end
+
+ def attribute_value(name, block)
+ api_value = api_resource&.dig(name)
+
+ if api_value && block
+ log_having_both_api_result_and_block(name, api_value)
+ end
+
+ api_value || (block && instance_exec(&block))
+ end
+
+ def log_having_both_api_result_and_block(name, api_value)
+ QA::Runtime::Logger.info "<#{self.class}> Attribute #{name.inspect} has both API response `#{api_value}` and a block. API response will be picked. Block will be ignored."
+ end
+
+ def self.fabricate!(*args, &prepare_block)
+ fabricate_via_api!(*args, &prepare_block)
+ rescue NotImplementedError
+ fabricate_via_browser_ui!(*args, &prepare_block)
+ end
+
+ def self.fabricate_via_browser_ui!(*args, &prepare_block)
+ options = args.extract_options!
+ resource = options.fetch(:resource) { new }
+ parents = options.fetch(:parents) { [] }
+
+ do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do
+ log_fabrication(:browser_ui, resource, parents, args) { resource.fabricate!(*args) }
+
+ current_url
+ end
+ end
+
+ def self.fabricate_via_api!(*args, &prepare_block)
+ options = args.extract_options!
+ resource = options.fetch(:resource) { new }
+ parents = options.fetch(:parents) { [] }
+
+ raise NotImplementedError unless resource.api_support?
+
+ resource.eager_load_api_client!
+
+ 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!(resource:, prepare_block:, parents: [])
+ prepare_block.call(resource) if prepare_block
+
+ resource_web_url = yield
+ resource.web_url = resource_web_url
+
+ resource
+ end
+ private_class_method :do_fabricate!
+
+ def self.log_fabrication(method, resource, parents, args)
+ return yield unless Runtime::Env.debug?
+
+ start = Time.now
+ prefix = "==#{'=' * parents.size}>"
+ msg = [prefix]
+ msg << "Built a #{name}"
+ msg << "as a dependency of #{parents.last}" if parents.any?
+ msg << "via #{method}"
+
+ yield.tap do
+ msg << "in #{Time.now - start} seconds"
+ puts msg.join(' ')
+ puts if parents.empty?
+ end
+ end
+ private_class_method :log_fabrication
+
+ def self.evaluator
+ @evaluator ||= Base::DSL.new(self)
+ end
+ private_class_method :evaluator
+
+ def self.dynamic_attributes
+ const_get(:DynamicAttributes)
+ rescue NameError
+ mod = const_set(:DynamicAttributes, Module.new)
+
+ include mod
+
+ mod
+ end
+
+ def self.attributes_names
+ dynamic_attributes.instance_methods(false).sort.grep_v(/=$/)
+ end
+
+ class DSL
+ def initialize(base)
+ @base = base
+ end
+
+ def attribute(name, &block)
+ @base.dynamic_attributes.module_eval do
+ attr_writer(name)
+
+ define_method(name) do
+ instance_variable_get("@#{name}") ||
+ instance_variable_set(
+ "@#{name}",
+ populate_attribute(name, block))
+ end
+ end
+ end
+ end
+
+ attribute :web_url
+ end
+ end
+end
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 6f878396f0e..c9fafe3419f 100644
--- a/qa/qa/factory/repository/project_push.rb
+++ b/qa/qa/resource/repository/project_push.rb
@@ -1,15 +1,16 @@
+# frozen_string_literal: true
+
module QA
- module Factory
+ module Resource
module Repository
- class ProjectPush < Factory::Repository::Push
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-with-code'
- project.description = 'Project with repository'
+ class ProjectPush < Repository::Push
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-with-code'
+ resource.description = 'Project with repository'
+ end
end
- product :output
- product :project
-
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 703c78daa99..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
@@ -45,7 +47,7 @@ module QA
repository.use_ssh_key(ssh_key)
else
repository.uri = repository_http_uri
- repository.use_default_credentials
+ repository.use_default_credentials unless user
end
username = 'GitLab QA'
diff --git a/qa/qa/factory/repository/wiki_push.rb b/qa/qa/resource/repository/wiki_push.rb
index ecc6cc18c88..f1c39d507fe 100644
--- a/qa/qa/factory/repository/wiki_push.rb
+++ b/qa/qa/resource/repository/wiki_push.rb
@@ -1,11 +1,15 @@
+# frozen_string_literal: true
+
module QA
- module Factory
+ module Resource
module Repository
- class WikiPush < Factory::Repository::Push
- dependency Factory::Resource::Wiki, as: :wiki do |wiki|
- wiki.title = 'Home'
- wiki.content = '# My First Wiki Content'
- wiki.message = 'Update home'
+ class WikiPush < Repository::Push
+ attribute :wiki do
+ Wiki.fabricate! do |resource|
+ resource.title = 'Home'
+ resource.content = '# My First Wiki Content'
+ resource.message = 'Update home'
+ end
end
def initialize
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 5e8f883e25f..40c06768ffe 100644
--- a/qa/qa/factory/settings/hashed_storage.rb
+++ b/qa/qa/resource/settings/hashed_storage.rb
@@ -1,13 +1,15 @@
+# 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)
- Page::Main::Login.act { sign_in_using_credentials }
- Page::Main::Menu.act { go_to_admin_area }
- Page::Admin::Menu.act { go_to_repository_settings }
+ Page::Main::Login.perform(&:sign_in_using_credentials)
+ Page::Main::Menu.perform(&:go_to_admin_area)
+ Page::Admin::Menu.perform(&:go_to_repository_settings)
Page::Admin::Settings::Repository.perform do |setting|
setting.expand_repository_storage do |page|
@@ -16,7 +18,7 @@ module QA
end
end
- QA::Page::Main::Menu.act { sign_out }
+ QA::Page::Main::Menu.perform(&:sign_out)
end
end
end
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..9be88ba4211
--- /dev/null
+++ b/qa/qa/resource/user.rb
@@ -0,0 +1,103 @@
+# 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
+
+ def self.fabricate_or_use(username, password)
+ if Runtime::Env.signup_disabled?
+ self.new.tap do |user|
+ user.username = username
+ user.password = password
+ end
+ else
+ self.fabricate!
+ end
+ 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..1154eaca6a9 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
@@ -65,6 +75,22 @@ module QA
ENV['GITLAB_FORKER_PASSWORD']
end
+ def gitlab_qa_username_1
+ ENV['GITLAB_QA_USERNAME_1'] || 'gitlab-qa-user1'
+ end
+
+ def gitlab_qa_password_1
+ ENV['GITLAB_QA_PASSWORD_1']
+ end
+
+ def gitlab_qa_username_2
+ ENV['GITLAB_QA_USERNAME_2'] || 'gitlab-qa-user2'
+ end
+
+ def gitlab_qa_password_2
+ ENV['GITLAB_QA_PASSWORD_2']
+ end
+
def ldap_username
ENV['GITLAB_LDAP_USERNAME']
end
@@ -104,6 +130,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/runtime/logger.rb b/qa/qa/runtime/logger.rb
index 3baa24de0ec..bd5c4fe5bf5 100644
--- a/qa/qa/runtime/logger.rb
+++ b/qa/qa/runtime/logger.rb
@@ -7,14 +7,16 @@ module QA
module Logger
extend SingleForwardable
- def_delegators :logger, :debug, :info, :error, :warn, :fatal, :unknown
+ def_delegators :logger, :debug, :info, :warn, :error, :fatal, :unknown
singleton_class.module_eval do
+ attr_writer :logger
+
def logger
return @logger if @logger
@logger = ::Logger.new Runtime::Env.log_destination
- @logger.level = ::Logger::DEBUG
+ @logger.level = Runtime::Env.debug? ? ::Logger::DEBUG : ::Logger::ERROR
@logger
end
end
diff --git a/qa/qa/scenario/test/integration/ldap_no_tls.rb b/qa/qa/scenario/test/integration/ldap_no_tls.rb
new file mode 100644
index 00000000000..bbf4c847f33
--- /dev/null
+++ b/qa/qa/scenario/test/integration/ldap_no_tls.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module QA
+ module Scenario
+ module Test
+ module Integration
+ class LDAPNoTLS < Test::Instance::All
+ tags :ldap_no_tls
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/scenario/test/integration/ldap.rb b/qa/qa/scenario/test/integration/ldap_tls.rb
index 769fa389785..2a767e57bc6 100644
--- a/qa/qa/scenario/test/integration/ldap.rb
+++ b/qa/qa/scenario/test/integration/ldap_tls.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
module QA
module Scenario
module Test
module Integration
- class LDAP < Test::Instance::All
- tags :ldap
+ class LDAPTLS < Test::Instance::All
+ tags :ldap_tls
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb
index eb9e0297287..a397df03bd2 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- context 'Manage', :orchestrated, :ldap do
+ context 'Manage', :orchestrated, :ldap_no_tls, :ldap_tls do
describe 'LDAP login' do
it 'user logs into GitLab using LDAP credentials' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb
index 45cb5df8252..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.
@@ -22,7 +22,7 @@ module QA
end
end
- context 'Manage', :orchestrated, :ldap, :skip_signup_disabled do
+ context 'Manage', :orchestrated, :ldap_no_tls, :skip_signup_disabled do
describe 'while LDAP is enabled' do
it_behaves_like 'registration and login'
end
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..4070a225260 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_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1)
- 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
new file mode 100644
index 00000000000..ac34f72bb8f
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Plan' do
+ describe 'filter issue comments activities' do
+ let(:issue_title) { 'issue title' }
+
+ it 'user filters comments and activites in an issue' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.act { sign_in_using_credentials }
+
+ Resource::Issue.fabricate! do |issue|
+ issue.title = issue_title
+ end
+
+ expect(page).to have_content(issue_title)
+
+ Page::Project::Issue::Show.perform do |show_page|
+ show_page.select_comments_only_filter
+ show_page.comment('/confidential')
+ show_page.comment('My own comment')
+
+ expect(show_page).not_to have_content("made the issue confidential")
+ expect(show_page).to have_content("My own comment")
+
+ show_page.select_all_activities_filter
+
+ 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
+ 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 46e1005829d..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
@@ -25,6 +25,7 @@ module QA
push.file_content = "Test with unicode characters ❤✓€❄"
end
+ Page::Project::Show.perform(&:wait_for_push)
merge_request.visit!
expect(page).to have_text('to be squashed')
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 9c64a9a3439..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
@@ -2,7 +2,7 @@
module QA
context 'Create' do
- describe 'Git clone over HTTP', :ldap do
+ describe 'Git clone over HTTP', :ldap_no_tls do
let(:location) do
Page::Project::Show.act do
choose_repository_clone_http
@@ -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
new file mode 100644
index 00000000000..a63b7dce8d6
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Create' do
+ describe 'Git push over HTTP', :ldap_no_tls do
+ it 'user using a personal access token pushes code to the repository' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform(&:sign_in_using_credentials)
+
+ access_token = Resource::PersonalAccessToken.fabricate!.access_token
+
+ user = Resource::User.new.tap do |user|
+ user.username = Runtime::User.username
+ user.password = access_token
+ end
+
+ push = Resource::Repository::ProjectPush.fabricate! do |push|
+ push.user = user
+ push.file_name = 'README.md'
+ push.file_content = '# This is a test project'
+ push.commit_message = 'Add README.md'
+ end
+
+ push.project.visit!
+ Page::Project::Show.perform(&:wait_for_push)
+
+ expect(page).to have_content('README.md')
+ expect(page).to have_content('This is a test project')
+ end
+ end
+ end
+end
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 b9bed39662f..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
@@ -2,12 +2,12 @@
module QA
context 'Create' do
- describe 'Git push over HTTP', :ldap do
+ describe 'Git push over HTTP', :ldap_no_tls do
it 'user pushes code to the repository' do
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 5f42cb00bd3..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
@@ -2,11 +2,11 @@
module QA
context 'Create' do
- describe 'Protected branch support', :ldap do
+ describe 'Protected branch support', :ldap_no_tls do
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/secret_variable/add_secret_variable_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb
index 292f24d9c0d..0837b720df1 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/secret_variable/add_secret_variable_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb
@@ -2,24 +2,24 @@
module QA
context 'Verify' do
- describe 'Secret variable support' do
- it 'user adds a secret variable' do
+ describe 'CI variable support' do
+ it 'user adds a CI variable' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Factory::Resource::SecretVariable.fabricate! do |resource|
+ Resource::CiVariable.fabricate! do |resource|
resource.key = 'VARIABLE_KEY'
- resource.value = 'some secret variable'
+ resource.value = 'some CI variable'
end
Page::Project::Settings::CICD.perform do |settings|
- settings.expand_secret_variables do |page|
+ settings.expand_ci_variables do |page|
expect(page).to have_field(with: 'VARIABLE_KEY')
- expect(page).not_to have_field(with: 'some secret variable')
+ expect(page).not_to have_field(with: 'some CI variable')
page.reveal_variables
- expect(page).to have_field(with: 'some secret variable')
+ expect(page).to have_field(with: 'some CI variable')
end
end
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 caf014c89ea..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::SecretVariable.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 c98ede25b68..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::SecretVariable.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
@@ -49,11 +49,13 @@ module QA
cluster.install_prometheus = true
cluster.install_runner = true
end
+ kubernetes_cluster.populate(:ingress_ip)
project.visit!
Page::Project::Menu.act { click_ci_cd_settings }
Page::Project::Settings::CICD.perform do |p|
- p.enable_auto_devops_with_domain("#{kubernetes_cluster.ingress_ip}.nip.io")
+ p.enable_auto_devops_with_domain(
+ "#{kubernetes_cluster.ingress_ip}.nip.io")
end
project.visit!
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 229f93a1041..00000000000
--- a/qa/spec/factory/base_spec.rb
+++ /dev/null
@@ -1,187 +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(:populate!).with(factory, product_location).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
-
- describe '.dependency' do
- let(:dependency) { spy('dependency') }
-
- before do
- stub_const('Some::MyDependency', dependency)
- end
-
- subject do
- Class.new(described_class) do
- dependency Some::MyDependency, as: :mydep do |factory|
- factory.something!
- end
- end
- end
-
- it 'appends a new dependency and accessors' do
- expect(subject.dependencies).to be_one
- end
-
- it 'defines dependency accessors' do
- expect(subject.new).to respond_to :mydep, :mydep=
- end
-
- describe 'dependencies fabrication' do
- let(:dependency) { double('dependency') }
- let(:instance) { spy('instance') }
-
- subject do
- Class.new(described_class) do
- dependency Some::MyDependency, as: :mydep
- end
- end
-
- before do
- stub_const('Some::MyDependency', dependency)
-
- allow(subject).to receive(:new).and_return(instance)
- allow(subject).to receive(:current_url).and_return(product_location)
- allow(instance).to receive(:mydep).and_return(nil)
- expect(QA::Factory::Product).to receive(:populate!)
- end
-
- it 'builds all dependencies first' do
- expect(dependency).to receive(:fabricate!).once
-
- subject.fabricate!
- end
- end
- end
-
- describe '.product' do
- include_context 'fabrication context'
-
- subject do
- Class.new(described_class) do
- def fabricate!
- "any"
- end
-
- product :token
- end
- end
-
- it 'appends new product attribute' do
- expect(subject.attributes).to be_one
- expect(subject.attributes[0]).to be_a(QA::Factory::Product::Attribute)
- expect(subject.attributes[0].name).to eq(:token)
- end
- end
-end
diff --git a/qa/spec/factory/dependency_spec.rb b/qa/spec/factory/dependency_spec.rb
deleted file mode 100644
index 657beddffb1..00000000000
--- a/qa/spec/factory/dependency_spec.rb
+++ /dev/null
@@ -1,79 +0,0 @@
-describe QA::Factory::Dependency do
- let(:dependency) { spy('dependency' ) }
- let(:factory) { spy('factory') }
- let(:block) { spy('block') }
-
- let(:signature) do
- double('signature', name: :mydep, factory: dependency, block: block)
- end
-
- subject do
- described_class.new(factory, signature)
- end
-
- describe '#overridden?' do
- it 'returns true if factory has overridden dependency' do
- allow(factory).to receive(:mydep).and_return('something')
-
- expect(subject).to be_overridden
- end
-
- it 'returns false if dependency has not been overridden' do
- allow(factory).to receive(:mydep).and_return(nil)
-
- expect(subject).not_to be_overridden
- end
- end
-
- describe '#build!' do
- context 'when dependency has been overridden' do
- before do
- allow(subject).to receive(:overridden?).and_return(true)
- end
-
- it 'does not fabricate dependency' do
- subject.build!
-
- expect(dependency).not_to have_received(:fabricate!)
- end
- end
-
- context 'when dependency has not been overridden' do
- before do
- allow(subject).to receive(:overridden?).and_return(false)
- end
-
- it 'fabricates dependency' do
- subject.build!
-
- expect(dependency).to have_received(:fabricate!)
- end
-
- it 'sets product in the factory' do
- subject.build!
-
- expect(factory).to have_received(:mydep=).with(dependency)
- end
-
- it 'calls given block with dependency factory and caller factory' do
- expect(dependency).to receive(:fabricate!).and_yield(dependency)
-
- subject.build!
-
- expect(block).to have_received(:call).with(dependency, factory)
- end
-
- context 'with no block given' do
- let(:signature) do
- double('signature', name: :mydep, factory: dependency, block: nil)
- end
-
- it 'does not error' do
- subject.build!
-
- expect(dependency).to have_received(:fabricate!)
- end
- end
- end
- end
-end
diff --git a/qa/spec/factory/product_spec.rb b/qa/spec/factory/product_spec.rb
deleted file mode 100644
index 43b1d93d769..00000000000
--- a/qa/spec/factory/product_spec.rb
+++ /dev/null
@@ -1,81 +0,0 @@
-describe QA::Factory::Product do
- let(:factory) do
- Class.new(QA::Factory::Base) do
- def foo
- 'bar'
- end
- end.new
- end
-
- let(:product) { spy('product') }
- let(:product_location) { 'http://product_location' }
-
- subject { described_class.new(factory, product_location) }
-
- describe '.populate!' do
- before do
- expect(factory.class).to receive(:attributes).and_return(attributes)
- end
-
- context 'when the product attribute is populated via a block' do
- let(:attributes) do
- [QA::Factory::Product::Attribute.new(:test, proc { 'returned' })]
- end
-
- it 'returns a fabrication product and defines factory attributes as its methods' do
- result = described_class.populate!(factory, product_location)
-
- expect(result).to be_a(described_class)
- expect(result.test).to eq('returned')
- end
- end
-
- context 'when the product attribute is populated via the api' do
- let(:attributes) do
- [QA::Factory::Product::Attribute.new(:test)]
- end
-
- it 'returns a fabrication product and defines factory attributes as its methods' do
- expect(factory).to receive(:api_resource).and_return({ test: 'returned' })
-
- result = described_class.populate!(factory, product_location)
-
- expect(result).to be_a(described_class)
- expect(result.test).to eq('returned')
- end
- end
-
- context 'when the product attribute is populated via a factory attribute' do
- let(:attributes) do
- [QA::Factory::Product::Attribute.new(:foo)]
- end
-
- it 'returns a fabrication product and defines factory attributes as its methods' do
- result = described_class.populate!(factory, product_location)
-
- expect(result).to be_a(described_class)
- expect(result.foo).to eq('bar')
- end
- end
-
- context 'when the product attribute has no value' do
- let(:attributes) do
- [QA::Factory::Product::Attribute.new(:bar)]
- end
-
- it 'returns a fabrication product and defines factory attributes as its methods' do
- expect { described_class.populate!(factory, product_location) }
- .to raise_error(described_class::NoValueError, "No value was computed for product bar of factory #{factory.class.name}.")
- end
- end
- 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/page/logging_spec.rb b/qa/spec/page/logging_spec.rb
index 9f17de4edbf..9d56353062b 100644
--- a/qa/spec/page/logging_spec.rb
+++ b/qa/spec/page/logging_spec.rb
@@ -3,9 +3,15 @@
require 'capybara/dsl'
describe QA::Support::Page::Logging do
+ include Support::StubENV
+
let(:page) { double().as_null_object }
before do
+ logger = Logger.new $stdout
+ logger.level = ::Logger::DEBUG
+ QA::Runtime::Logger.logger = logger
+
allow(Capybara).to receive(:current_session).and_return(page)
allow(page).to receive(:current_url).and_return('http://current-url')
allow(page).to receive(:has_css?).with(any_args).and_return(true)
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/runtime/logger_spec.rb b/qa/spec/runtime/logger_spec.rb
index 794e1f9bfe6..44be3381bff 100644
--- a/qa/spec/runtime/logger_spec.rb
+++ b/qa/spec/runtime/logger_spec.rb
@@ -1,6 +1,12 @@
# frozen_string_literal: true
describe QA::Runtime::Logger do
+ before do
+ logger = Logger.new $stdout
+ logger.level = ::Logger::DEBUG
+ described_class.logger = logger
+ end
+
it 'logs debug' do
expect { described_class.debug('test') }.to output(/DEBUG -- : test/).to_stdout_from_any_process
end
diff --git a/qa/spec/scenario/test/integration/ldap_spec.rb b/qa/spec/scenario/test/integration/ldap_spec.rb
index 198856aec3f..b6d798bf504 100644
--- a/qa/spec/scenario/test/integration/ldap_spec.rb
+++ b/qa/spec/scenario/test/integration/ldap_spec.rb
@@ -1,9 +1,17 @@
# frozen_string_literal: true
-describe QA::Scenario::Test::Integration::LDAP do
+describe QA::Scenario::Test::Integration::LDAPNoTLS do
context '#perform' do
it_behaves_like 'a QA scenario class' do
- let(:tags) { [:ldap] }
+ let(:tags) { [:ldap_no_tls] }
+ end
+ end
+end
+
+describe QA::Scenario::Test::Integration::LDAPTLS do
+ context '#perform' do
+ it_behaves_like 'a QA scenario class' do
+ let(:tags) { [:ldap_tls] }
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..4e5ef977161
--- /dev/null
+++ b/scripts/build_assets_image
@@ -0,0 +1,27 @@
+#!/bin/bash
+
+# Exit early if we don't want to build the image
+if [[ "${BUILD_ASSETS_IMAGE}" != "true" ]]
+then
+ exit 0
+fi
+
+# 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/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index be3fc832008..efc3ce74627 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -650,7 +650,7 @@ describe ApplicationController do
describe '#access_denied' do
controller(described_class) do
def index
- access_denied!(params[:message])
+ access_denied!(params[:message], params[:status])
end
end
@@ -669,6 +669,12 @@ describe ApplicationController do
expect(response).to have_gitlab_http_status(403)
end
+
+ it 'renders a status passed to access denied' do
+ get :index, status: 401
+
+ expect(response).to have_gitlab_http_status(401)
+ end
end
context 'when invalid UTF-8 parameters are received' do
@@ -792,4 +798,30 @@ describe ApplicationController do
end
end
end
+
+ context 'control headers' do
+ controller(described_class) do
+ def index
+ render json: :ok
+ end
+ end
+
+ context 'user not logged in' do
+ it 'sets the default headers' do
+ get :index
+
+ expect(response.headers['Cache-Control']).to be_nil
+ end
+ end
+
+ context 'user logged in' do
+ it 'sets the default headers' do
+ sign_in(user)
+
+ get :index
+
+ expect(response.headers['Cache-Control']).to eq 'max-age=0, private, must-revalidate, no-store'
+ end
+ end
+ end
end
diff --git a/spec/controllers/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb
index c365988a100..98946e4287b 100644
--- a/spec/controllers/boards/issues_controller_spec.rb
+++ b/spec/controllers/boards/issues_controller_spec.rb
@@ -208,11 +208,22 @@ describe Boards::IssuesController do
end
end
- context 'with unauthorized user' do
- it 'returns a forbidden 403 response' do
- create_issue user: guest, board: board, list: list1, title: 'New issue'
+ context 'with guest user' do
+ context 'in open list' do
+ it 'returns a successful 200 response' do
+ open_list = board.lists.create(list_type: :backlog)
+ create_issue user: guest, board: board, list: open_list, title: 'New issue'
- expect(response).to have_gitlab_http_status(403)
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ context 'in label list' do
+ it 'returns a forbidden 403 response' do
+ create_issue user: guest, board: board, list: list1, title: 'New issue'
+
+ expect(response).to have_gitlab_http_status(403)
+ end
end
end
diff --git a/spec/controllers/concerns/issuable_collections_spec.rb b/spec/controllers/concerns/issuable_collections_spec.rb
index d16a3464495..e93c923fd39 100644
--- a/spec/controllers/concerns/issuable_collections_spec.rb
+++ b/spec/controllers/concerns/issuable_collections_spec.rb
@@ -60,7 +60,7 @@ describe IssuableCollections do
end
end
- describe '#filter_params' do
+ describe '#finder_options' do
let(:params) do
{
assignee_id: '1',
@@ -84,25 +84,20 @@ describe IssuableCollections do
}
end
- it 'filters params' do
+ it 'only allows whitelisted params' do
allow(controller).to receive(:cookies).and_return({})
- filtered_params = controller.send(:filter_params)
+ finder_options = controller.send(:finder_options)
- expect(filtered_params).to eq({
+ expect(finder_options).to eq({
'assignee_id' => '1',
'assignee_username' => 'user1',
'author_id' => '2',
'author_username' => 'user2',
- 'authorized_only' => 'true',
- 'due_date' => '2017-01-01',
- 'group_id' => '3',
- 'iids' => '4',
'label_name' => 'foo',
'milestone_title' => 'bar',
'my_reaction_emoji' => 'thumbsup',
- 'non_archived' => 'true',
- 'project_id' => '5',
+ 'due_date' => '2017-01-01',
'scope' => 'all',
'search' => 'baz',
'sort' => 'priority',
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/dashboard/milestones_controller_spec.rb b/spec/controllers/dashboard/milestones_controller_spec.rb
index 56047c0c8d2..278b980b6d8 100644
--- a/spec/controllers/dashboard/milestones_controller_spec.rb
+++ b/spec/controllers/dashboard/milestones_controller_spec.rb
@@ -45,6 +45,8 @@ describe Dashboard::MilestonesController do
end
describe "#index" do
+ render_views
+
it 'returns group and project milestones to which the user belongs' do
get :index, format: :json
@@ -53,5 +55,12 @@ describe Dashboard::MilestonesController do
expect(json_response.map { |i| i["first_milestone"]["id"] }).to match_array([group_milestone.id, project_milestone.id])
expect(json_response.map { |i| i["group_name"] }.compact).to match_array(group.name)
end
+
+ it 'should contain group and project milestones to which the user belongs to' do
+ get :index
+
+ expect(response.body).to include("Open\n<span class=\"badge badge-pill\">3</span>")
+ expect(response.body).to include("Closed\n<span class=\"badge badge-pill\">0</span>")
+ end
end
end
diff --git a/spec/controllers/groups/boards_controller_spec.rb b/spec/controllers/groups/boards_controller_spec.rb
index bf41aa0706f..99429c93b82 100644
--- a/spec/controllers/groups/boards_controller_spec.rb
+++ b/spec/controllers/groups/boards_controller_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Groups::BoardsController do
let(:group) { create(:group) }
- let(:user) { create(:user) }
+ let(:user) { create(:user) }
before do
group.add_maintainer(user)
@@ -22,6 +22,28 @@ describe Groups::BoardsController do
expect(response.content_type).to eq 'text/html'
end
+ it 'redirects to latest visited board' do
+ board = create(:board, group: group)
+ create(:board_group_recent_visit, group: board.group, board: board, user: user)
+
+ list_boards
+
+ expect(response).to redirect_to(group_board_path(id: board.id))
+ end
+
+ it 'renders template if visited board is not found' do
+ 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_any_instance_of(Boards::Visits::LatestService).to receive(:execute).and_return(visited)
+
+ list_boards
+
+ expect(response).to render_template :index
+ expect(response.content_type).to eq 'text/html'
+ end
+
context 'with unauthorized user' do
before do
allow(Ability).to receive(:allowed?).with(user, :read_cross_project, :global).and_return(true)
@@ -35,12 +57,30 @@ describe Groups::BoardsController do
expect(response.content_type).to eq 'text/html'
end
end
+
+ context 'when user is signed out' do
+ let(:group) { create(:group, :public) }
+
+ it 'renders template' do
+ sign_out(user)
+
+ board = create(:board, group: group)
+ create(:board_group_recent_visit, group: board.group, board: board, user: user)
+
+ list_boards
+
+ expect(response).to render_template :index
+ expect(response.content_type).to eq 'text/html'
+ end
+ end
end
context 'when format is JSON' do
it 'return an array with one group board' do
create(:board, group: group)
+ expect(Boards::Visits::LatestService).not_to receive(:new)
+
list_boards format: :json
parsed_response = JSON.parse(response.body)
@@ -74,7 +114,7 @@ describe Groups::BoardsController do
context 'when format is HTML' do
it 'renders template' do
- read_board board: board
+ expect { read_board board: board }.to change(BoardGroupRecentVisit, :count).by(1)
expect(response).to render_template :show
expect(response.content_type).to eq 'text/html'
@@ -93,10 +133,25 @@ describe Groups::BoardsController do
expect(response.content_type).to eq 'text/html'
end
end
+
+ context 'when user is signed out' do
+ let(:group) { create(:group, :public) }
+
+ it 'does not save visit' do
+ sign_out(user)
+
+ expect { read_board board: board }.to change(BoardGroupRecentVisit, :count).by(0)
+
+ expect(response).to render_template :show
+ expect(response.content_type).to eq 'text/html'
+ end
+ end
end
context 'when format is JSON' do
it 'returns project board' do
+ expect(Boards::Visits::CreateService).not_to receive(:new)
+
read_board board: board, format: :json
expect(response).to match_response_schema('board')
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/profiles/personal_access_tokens_controller_spec.rb b/spec/controllers/profiles/personal_access_tokens_controller_spec.rb
index ed08a4c1bf2..f5860d4296b 100644
--- a/spec/controllers/profiles/personal_access_tokens_controller_spec.rb
+++ b/spec/controllers/profiles/personal_access_tokens_controller_spec.rb
@@ -39,8 +39,10 @@ describe Profiles::PersonalAccessTokensController do
let!(:active_personal_access_token) { create(:personal_access_token, user: user) }
let!(:inactive_personal_access_token) { create(:personal_access_token, :revoked, user: user) }
let!(:impersonation_personal_access_token) { create(:personal_access_token, :impersonation, user: user) }
+ let(:token_value) { 's3cr3t' }
before do
+ PersonalAccessToken.redis_store!(user.id, token_value)
get :index
end
@@ -56,5 +58,9 @@ describe Profiles::PersonalAccessTokensController do
expect(assigns(:active_personal_access_tokens)).not_to include(impersonation_personal_access_token)
expect(assigns(:inactive_personal_access_tokens)).not_to include(impersonation_personal_access_token)
end
+
+ it "retrieves newly created personal access token value" do
+ expect(assigns(:new_personal_access_token)).to eql(token_value)
+ end
end
end
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 28f7e4634a5..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
diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb
index 096efc1c7b2..8d503f6ad32 100644
--- a/spec/controllers/projects/boards_controller_spec.rb
+++ b/spec/controllers/projects/boards_controller_spec.rb
@@ -28,6 +28,28 @@ describe Projects::BoardsController do
expect(response.content_type).to eq 'text/html'
end
+ it 'redirects to latest visited board' do
+ board = create(:board, project: project)
+ create(:board_project_recent_visit, project: board.project, board: board, user: user)
+
+ list_boards
+
+ expect(response).to redirect_to(namespace_project_board_path(id: board.id))
+ end
+
+ it 'renders template if visited board is not found' do
+ 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_any_instance_of(Boards::Visits::LatestService).to receive(:execute).and_return(visited)
+
+ list_boards
+
+ expect(response).to render_template :index
+ expect(response.content_type).to eq 'text/html'
+ end
+
context 'with unauthorized user' do
before do
allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
@@ -41,12 +63,30 @@ describe Projects::BoardsController do
expect(response.content_type).to eq 'text/html'
end
end
+
+ context 'when user is signed out' do
+ let(:project) { create(:project, :public) }
+
+ it 'renders template' do
+ sign_out(user)
+
+ board = create(:board, project: project)
+ create(:board_project_recent_visit, project: board.project, board: board, user: user)
+
+ list_boards
+
+ expect(response).to render_template :index
+ expect(response.content_type).to eq 'text/html'
+ end
+ end
end
context 'when format is JSON' do
it 'returns a list of project boards' do
create_list(:board, 2, project: project)
+ expect(Boards::Visits::LatestService).not_to receive(:new)
+
list_boards format: :json
parsed_response = JSON.parse(response.body)
@@ -98,7 +138,7 @@ describe Projects::BoardsController do
context 'when format is HTML' do
it 'renders template' do
- read_board board: board
+ expect { read_board board: board }.to change(BoardProjectRecentVisit, :count).by(1)
expect(response).to render_template :show
expect(response.content_type).to eq 'text/html'
@@ -117,10 +157,25 @@ describe Projects::BoardsController do
expect(response.content_type).to eq 'text/html'
end
end
+
+ context 'when user is signed out' do
+ let(:project) { create(:project, :public) }
+
+ it 'does not save visit' do
+ sign_out(user)
+
+ expect { read_board board: board }.to change(BoardProjectRecentVisit, :count).by(0)
+
+ expect(response).to render_template :show
+ expect(response.content_type).to eq 'text/html'
+ end
+ end
end
context 'when format is JSON' do
it 'returns project board' do
+ expect(Boards::Visits::CreateService).not_to receive(:new)
+
read_board board: board, format: :json
expect(response).to match_response_schema('board')
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/deploy_keys_controller_spec.rb b/spec/controllers/projects/deploy_keys_controller_spec.rb
index 73bf169085f..4567a51b88e 100644
--- a/spec/controllers/projects/deploy_keys_controller_spec.rb
+++ b/spec/controllers/projects/deploy_keys_controller_spec.rb
@@ -27,12 +27,8 @@ describe Projects::DeployKeysController do
let(:project2) { create(:project, :internal)}
let(:project_private) { create(:project, :private)}
- let(:deploy_key_internal) do
- create(:deploy_key, key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCdMHEHyhRjbhEZVddFn6lTWdgEy5Q6Bz4nwGB76xWZI5YT/1WJOMEW+sL5zYd31kk7sd3FJ5L9ft8zWMWrr/iWXQikC2cqZK24H1xy+ZUmrRuJD4qGAaIVoyyzBL+avL+lF8J5lg6YSw8gwJY/lX64/vnJHUlWw2n5BF8IFOWhiw== dummy@gitlab.com')
- end
- let(:deploy_key_actual) do
- create(:deploy_key, key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDNd/UJWhPrpb+b/G5oL109y57yKuCxE+WUGJGYaj7WQKsYRJmLYh1mgjrl+KVyfsWpq4ylOxIfFSnN9xBBFN8mlb0Fma5DC7YsSsibJr3MZ19ZNBprwNcdogET7aW9I0In7Wu5f2KqI6e5W/spJHCy4JVxzVMUvk6Myab0LnJ2iQ== dummy@gitlab.com')
- end
+ let(:deploy_key_internal) { create(:deploy_key) }
+ let(:deploy_key_actual) { create(:deploy_key) }
let!(:deploy_key_public) { create(:deploy_key, public: true) }
let!(:deploy_keys_project_internal) do
@@ -63,4 +59,145 @@ describe Projects::DeployKeysController do
end
end
end
+
+ describe '/enable/:id' do
+ let(:deploy_key) { create(:deploy_key) }
+ let(:project2) { create(:project) }
+ let!(:deploy_keys_project_internal) do
+ create(:deploy_keys_project, project: project2, deploy_key: deploy_key)
+ end
+
+ context 'with anonymous user' do
+ before do
+ sign_out(:user)
+ end
+
+ it 'redirects to login' do
+ expect do
+ put :enable, id: deploy_key.id, namespace_id: project.namespace, project_id: project
+ end.not_to change { DeployKeysProject.count }
+
+ expect(response).to have_http_status(302)
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
+ context 'with user with no permission' do
+ before do
+ sign_in(create(:user))
+ end
+
+ it 'returns 404' do
+ expect do
+ put :enable, id: deploy_key.id, namespace_id: project.namespace, project_id: project
+ end.not_to change { DeployKeysProject.count }
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'with user with permission' do
+ before do
+ project2.add_maintainer(user)
+ end
+
+ it 'returns 302' do
+ expect do
+ put :enable, id: deploy_key.id, namespace_id: project.namespace, project_id: project
+ end.to change { DeployKeysProject.count }.by(1)
+
+ expect(DeployKeysProject.where(project_id: project.id, deploy_key_id: deploy_key.id).count).to eq(1)
+ expect(response).to have_http_status(302)
+ expect(response).to redirect_to(namespace_project_settings_repository_path(anchor: 'js-deploy-keys-settings'))
+ end
+
+ it 'returns 404' do
+ put :enable, id: 0, namespace_id: project.namespace, project_id: project
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'with admin' do
+ before do
+ sign_in(create(:admin))
+ end
+
+ it 'returns 302' do
+ expect do
+ put :enable, id: deploy_key.id, namespace_id: project.namespace, project_id: project
+ end.to change { DeployKeysProject.count }.by(1)
+
+ expect(DeployKeysProject.where(project_id: project.id, deploy_key_id: deploy_key.id).count).to eq(1)
+ expect(response).to have_http_status(302)
+ expect(response).to redirect_to(namespace_project_settings_repository_path(anchor: 'js-deploy-keys-settings'))
+ end
+ end
+ end
+
+ describe '/disable/:id' do
+ let(:deploy_key) { create(:deploy_key) }
+ let!(:deploy_key_project) { create(:deploy_keys_project, project: project, deploy_key: deploy_key) }
+
+ context 'with anonymous user' do
+ before do
+ sign_out(:user)
+ end
+
+ it 'redirects to login' do
+ put :disable, id: deploy_key.id, namespace_id: project.namespace, project_id: project
+
+ expect(response).to have_http_status(302)
+ expect(response).to redirect_to(new_user_session_path)
+ expect(DeployKey.find(deploy_key.id)).to eq(deploy_key)
+ end
+ end
+
+ context 'with user with no permission' do
+ before do
+ sign_in(create(:user))
+ end
+
+ it 'returns 404' do
+ put :disable, id: deploy_key.id, namespace_id: project.namespace, project_id: project
+
+ expect(response).to have_http_status(404)
+ expect(DeployKey.find(deploy_key.id)).to eq(deploy_key)
+ end
+ end
+
+ context 'with user with permission' do
+ it 'returns 302' do
+ put :disable, id: deploy_key.id, namespace_id: project.namespace, project_id: project
+
+ expect(response).to have_http_status(302)
+ expect(response).to redirect_to(namespace_project_settings_repository_path(anchor: 'js-deploy-keys-settings'))
+
+ expect { DeployKey.find(deploy_key.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'returns 404' do
+ put :disable, id: 0, namespace_id: project.namespace, project_id: project
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'with admin' do
+ before do
+ sign_in(create(:admin))
+ end
+
+ it 'returns 302' do
+ expect do
+ put :disable, id: deploy_key.id, namespace_id: project.namespace, project_id: project
+ end.to change { DeployKey.count }.by(-1)
+
+ expect(response).to have_http_status(302)
+ expect(response).to redirect_to(namespace_project_settings_repository_path(anchor: 'js-deploy-keys-settings'))
+
+ expect { DeployKey.find(deploy_key.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ 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 1484676eea3..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)
@@ -297,6 +309,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
expect(response).to match_response_schema('job/job_details')
expect(json_response['runners']['online']).to be false
expect(json_response['runners']['available']).to be false
+ expect(json_response['stuck']).to be true
end
end
@@ -309,6 +322,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
expect(response).to match_response_schema('job/job_details')
expect(json_response['runners']['online']).to be false
expect(json_response['runners']['available']).to be true
+ expect(json_response['stuck']).to be true
end
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index dcfd6c05200..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
@@ -749,13 +750,15 @@ describe Projects::MergeRequestsController do
describe 'GET ci_environments_status' do
context 'the environment is from a forked project' do
- let!(:forked) { fork_project(project, user, repository: true) }
- let!(:environment) { create(:environment, project: forked) }
- let!(:deployment) { create(:deployment, environment: environment, sha: forked.commit.id, ref: 'master') }
- let(:admin) { create(:admin) }
+ let(:forked) { fork_project(project, user, repository: true) }
+ let(:sha) { forked.commit.sha }
+ 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, :succeed, environment: environment, sha: sha, ref: 'master', deployable: build) }
let(:merge_request) do
- create(:merge_request, source_project: forked, target_project: project)
+ create(:merge_request, source_project: forked, target_project: project, target_branch: 'master', head_pipeline: pipeline)
end
it 'links to the environment on that project' do
@@ -764,6 +767,35 @@ describe Projects::MergeRequestsController do
expect(json_response.first['url']).to match /#{forked.full_path}/
end
+ context "when environment_target is 'merge_commit'" do
+ it 'returns nothing' do
+ get_ci_environments_status(environment_target: 'merge_commit')
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_empty
+ end
+
+ context 'when is merged' do
+ let(:source_environment) { create(:environment, project: project) }
+ 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, :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 environment on the source project' do
+ get_ci_environments_status(environment_target: 'merge_commit')
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.first['url']).to match /#{project.full_path}/
+ end
+ end
+ end
+
# we're trying to reduce the overall number of queries for this method.
# set a hard limit for now. https://gitlab.com/gitlab-org/gitlab-ce/issues/52287
it 'keeps queries in check' do
@@ -772,11 +804,15 @@ describe Projects::MergeRequestsController do
expect(control_count).to be <= 137
end
- def get_ci_environments_status
- get :ci_environments_status,
+ def get_ci_environments_status(extra_params = {})
+ params = {
namespace_id: merge_request.project.namespace.to_param,
project_id: merge_request.project,
- id: merge_request.iid, format: 'json'
+ id: merge_request.iid,
+ format: 'json'
+ }
+
+ get :ci_environments_status, params.merge(extra_params)
end
end
end
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/controllers/projects/mirrors_controller_spec.rb b/spec/controllers/projects/mirrors_controller_spec.rb
index 6114eef7003..00c1e617e3a 100644
--- a/spec/controllers/projects/mirrors_controller_spec.rb
+++ b/spec/controllers/projects/mirrors_controller_spec.rb
@@ -63,6 +63,69 @@ describe Projects::MirrorsController do
end
end
+ describe '#ssh_host_keys', :use_clean_rails_memory_store_caching do
+ let(:project) { create(:project) }
+ let(:cache) { SshHostKey.new(project: project, url: "ssh://example.com:22") }
+
+ before do
+ sign_in(project.owner)
+ end
+
+ context 'invalid URLs' do
+ %w[
+ INVALID
+ git@example.com:foo/bar.git
+ ssh://git@example.com:foo/bar.git
+ ].each do |url|
+ it "returns an error with a 400 response for URL #{url.inspect}" do
+ do_get(project, url)
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response).to eq('message' => 'Invalid URL')
+ end
+ end
+ end
+
+ context 'no data in cache' do
+ it 'requests the cache to be filled and returns a 204 response' do
+ expect(ReactiveCachingWorker).to receive(:perform_async).with(cache.class, cache.id).at_least(:once)
+
+ do_get(project)
+
+ expect(response).to have_gitlab_http_status(204)
+ end
+ end
+
+ context 'error in the cache' do
+ it 'returns the error with a 400 response' do
+ stub_reactive_cache(cache, error: 'An error')
+
+ do_get(project)
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response).to eq('message' => 'An error')
+ end
+ end
+
+ context 'data in the cache' do
+ let(:ssh_key) { 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf' }
+ let(:ssh_fp) { { type: 'ed25519', bits: 256, fingerprint: '2e:65:6a:c8:cf:bf:b2:8b:9a:bd:6d:9f:11:5c:12:16', index: 0 } }
+
+ it 'returns the data with a 200 response' do
+ stub_reactive_cache(cache, known_hosts: ssh_key)
+
+ do_get(project)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to eq('known_hosts' => ssh_key, 'fingerprints' => [ssh_fp.stringify_keys], 'host_keys_changed' => true)
+ end
+ end
+
+ def do_get(project, url = 'ssh://example.com')
+ get :ssh_host_keys, namespace_id: project.namespace, project_id: project, ssh_url: url
+ end
+ end
+
def do_put(project, options, extra_attrs = {})
attrs = extra_attrs.merge(namespace_id: project.namespace.to_param, project_id: project.to_param)
attrs[:project] = options
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index 898f3863008..d334a2ff566 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -49,7 +49,7 @@ describe RegistrationsController do
end
it 'displays an error when the reCAPTCHA is not solved' do
- # Without this, `verify_recaptcha` arbitraily returns true in test env
+ # Without this, `verify_recaptcha` arbitrarily returns true in test env
Recaptcha.configuration.skip_verify_env.delete('test')
post(:create, user_params)
diff --git a/spec/controllers/root_controller_spec.rb b/spec/controllers/root_controller_spec.rb
index 7688538a468..995f803d757 100644
--- a/spec/controllers/root_controller_spec.rb
+++ b/spec/controllers/root_controller_spec.rb
@@ -98,7 +98,7 @@ describe RootController do
it 'redirects to their assigned issues' do
get :index
- expect(response).to redirect_to issues_dashboard_path(assignee_id: user.id)
+ expect(response).to redirect_to issues_dashboard_path(assignee_username: user.username)
end
end
@@ -110,7 +110,7 @@ describe RootController do
it 'redirects to their assigned merge requests' do
get :index
- expect(response).to redirect_to merge_requests_dashboard_path(assignee_id: user.id)
+ expect(response).to redirect_to merge_requests_dashboard_path(assignee_username: user.username)
end
end
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index 8e25b61e2f1..c691b3f478b 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -89,7 +89,7 @@ describe SessionsController do
end
it 'displays an error when the reCAPTCHA is not solved' do
- # Without this, `verify_recaptcha` arbitraily returns true in test env
+ # Without this, `verify_recaptcha` arbitrarily returns true in test env
Recaptcha.configuration.skip_verify_env.delete('test')
counter = double(:counter)
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb
index bcf289f36a9..6420b70a54f 100644
--- a/spec/controllers/uploads_controller_spec.rb
+++ b/spec/controllers/uploads_controller_spec.rb
@@ -5,6 +5,17 @@ shared_examples 'content not cached without revalidation' do
end
end
+shared_examples 'content not cached without revalidation and no-store' do
+ it 'ensures content will not be cached without revalidation' do
+ # Fixed in newer versions of ActivePack, it will only output a single `private`.
+ if Gitlab.rails5?
+ expect(subject['Cache-Control']).to eq('max-age=0, private, must-revalidate, no-store')
+ else
+ expect(subject['Cache-Control']).to eq('max-age=0, private, must-revalidate, private, no-store')
+ end
+ end
+end
+
describe UploadsController do
let!(:user) { create(:user, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) }
@@ -177,7 +188,7 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(200)
end
- it_behaves_like 'content not cached without revalidation' do
+ it_behaves_like 'content not cached without revalidation and no-store' do
subject do
get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'dk.png'
@@ -239,7 +250,7 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(200)
end
- it_behaves_like 'content not cached without revalidation' do
+ it_behaves_like 'content not cached without revalidation and no-store' do
subject do
get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'dk.png'
@@ -292,7 +303,7 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(200)
end
- it_behaves_like 'content not cached without revalidation' do
+ it_behaves_like 'content not cached without revalidation and no-store' do
subject do
get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'dk.png'
@@ -344,7 +355,7 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(200)
end
- it_behaves_like 'content not cached without revalidation' do
+ it_behaves_like 'content not cached without revalidation and no-store' do
subject do
get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'dk.png'
@@ -388,7 +399,7 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(200)
end
- it_behaves_like 'content not cached without revalidation' do
+ it_behaves_like 'content not cached without revalidation and no-store' do
subject do
get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'dk.png'
@@ -445,7 +456,7 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(200)
end
- it_behaves_like 'content not cached without revalidation' do
+ it_behaves_like 'content not cached without revalidation and no-store' do
subject do
get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png'
@@ -498,7 +509,7 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(200)
end
- it_behaves_like 'content not cached without revalidation' do
+ it_behaves_like 'content not cached without revalidation and no-store' do
subject do
get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png'
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
new file mode 100644
index 00000000000..e8584846b56
--- /dev/null
+++ b/spec/db/schema_spec.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Database schema' do
+ let(:connection) { ActiveRecord::Base.connection }
+ let(:tables) { connection.tables }
+
+ # Use if you are certain that this column should not have a foreign key
+ IGNORED_FK_COLUMNS = {
+ abuse_reports: %w[reporter_id user_id],
+ application_settings: %w[performance_bar_allowed_group_id],
+ audit_events: %w[author_id entity_id],
+ award_emoji: %w[awardable_id user_id],
+ chat_names: %w[chat_id service_id team_id user_id],
+ chat_teams: %w[team_id],
+ ci_builds: %w[erased_by_id runner_id trigger_request_id user_id],
+ ci_pipelines: %w[user_id],
+ ci_runner_projects: %w[runner_id],
+ ci_trigger_requests: %w[commit_id],
+ cluster_providers_gcp: %w[gcp_project_id operation_id],
+ deploy_keys_projects: %w[deploy_key_id],
+ deployments: %w[deployable_id environment_id user_id],
+ emails: %w[user_id],
+ events: %w[target_id],
+ forked_project_links: %w[forked_from_project_id],
+ identities: %w[user_id],
+ issues: %w[last_edited_by_id],
+ keys: %w[user_id],
+ label_links: %w[target_id],
+ lfs_objects_projects: %w[lfs_object_id project_id],
+ members: %w[source_id created_by_id],
+ merge_requests: %w[last_edited_by_id],
+ namespaces: %w[owner_id parent_id],
+ notes: %w[author_id commit_id noteable_id updated_by_id resolved_by_id discussion_id],
+ notification_settings: %w[source_id],
+ oauth_access_grants: %w[resource_owner_id application_id],
+ oauth_access_tokens: %w[resource_owner_id application_id],
+ oauth_applications: %w[owner_id],
+ project_group_links: %w[group_id],
+ project_statistics: %w[namespace_id],
+ projects: %w[creator_id namespace_id ci_id],
+ redirect_routes: %w[source_id],
+ repository_languages: %w[programming_language_id],
+ routes: %w[source_id],
+ sent_notifications: %w[project_id noteable_id recipient_id commit_id in_reply_to_discussion_id],
+ snippets: %w[author_id],
+ spam_logs: %w[user_id],
+ subscriptions: %w[user_id subscribable_id],
+ taggings: %w[tag_id taggable_id tagger_id],
+ timelogs: %w[user_id],
+ todos: %w[target_id commit_id],
+ uploads: %w[model_id],
+ user_agent_details: %w[subject_id],
+ users: %w[color_scheme_id created_by_id theme_id],
+ users_star_projects: %w[user_id],
+ web_hooks: %w[service_id]
+ }.with_indifferent_access.freeze
+
+ context 'for table' do
+ ActiveRecord::Base.connection.tables.sort.each do |table|
+ describe table do
+ let(:indexes) { connection.indexes(table) }
+ let(:columns) { connection.columns(table) }
+ let(:foreign_keys) { connection.foreign_keys(table) }
+
+ context 'all foreign keys' do
+ # for index to be effective, the FK constraint has to be at first place
+ it 'are indexed' do
+ first_indexed_column = indexes.map(&:columns).map(&:first)
+ foreign_keys_columns = foreign_keys.map(&:column)
+
+ expect(first_indexed_column.uniq).to include(*foreign_keys_columns)
+ end
+ end
+
+ context 'columns ending with _id' do
+ let(:column_names) { columns.map(&:name) }
+ let(:column_names_with_id) { column_names.select { |column_name| column_name.ends_with?('_id') } }
+ let(:foreign_keys_columns) { foreign_keys.map(&:column) }
+ let(:ignored_columns) { ignored_fk_columns(table) }
+
+ it 'do have the foreign keys' do
+ expect(column_names_with_id - ignored_columns).to contain_exactly(*foreign_keys_columns)
+ end
+ end
+ end
+ end
+ end
+
+ private
+
+ def ignored_fk_columns(column)
+ IGNORED_FK_COLUMNS.fetch(column, [])
+ end
+end
diff --git a/spec/factories/board_group_recent_visit.rb b/spec/factories/board_group_recent_visit.rb
new file mode 100644
index 00000000000..97ad5d6d068
--- /dev/null
+++ b/spec/factories/board_group_recent_visit.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :board_group_recent_visit do
+ user
+ group
+ board
+ end
+end
diff --git a/spec/factories/board_project_recent_visit.rb b/spec/factories/board_project_recent_visit.rb
new file mode 100644
index 00000000000..49ae4d7b391
--- /dev/null
+++ b/spec/factories/board_project_recent_visit.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :board_project_recent_visit do
+ user
+ project
+ board
+ end
+end
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/clusters.rb b/spec/factories/clusters/clusters.rb
index bbeba8ce8b9..c9f5d0a813e 100644
--- a/spec/factories/clusters/clusters.rb
+++ b/spec/factories/clusters/clusters.rb
@@ -2,13 +2,28 @@ FactoryBot.define do
factory :cluster, class: Clusters::Cluster do
user
name 'test-cluster'
+ cluster_type :project_type
+
+ trait :instance do
+ cluster_type { Clusters::Cluster.cluster_types[:instance_type] }
+ end
trait :project do
+ cluster_type { Clusters::Cluster.cluster_types[:project_type] }
+
before(:create) do |cluster, evaluator|
cluster.projects << create(:project, :repository)
end
end
+ trait :group do
+ cluster_type { Clusters::Cluster.cluster_types[:group_type] }
+
+ before(:create) do |cluster, evalutor|
+ cluster.groups << create(:group)
+ end
+ end
+
trait :provided_by_user do
provider_type :user
platform_type :kubernetes
diff --git a/spec/factories/clusters/kubernetes_namespaces.rb b/spec/factories/clusters/kubernetes_namespaces.rb
index 6fdada75a3d..6ad93fb0f45 100644
--- a/spec/factories/clusters/kubernetes_namespaces.rb
+++ b/spec/factories/clusters/kubernetes_namespaces.rb
@@ -2,8 +2,17 @@
FactoryBot.define do
factory :cluster_kubernetes_namespace, class: Clusters::KubernetesNamespace do
- cluster
- project
- cluster_project
+ association :cluster, :project, :provided_by_gcp
+
+ 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/fast_spec_helper.rb b/spec/fast_spec_helper.rb
index 134eb25e4b1..0b5ab16ad71 100644
--- a/spec/fast_spec_helper.rb
+++ b/spec/fast_spec_helper.rb
@@ -8,3 +8,5 @@ require_relative 'support/rspec'
require 'active_support/all'
ActiveSupport::Dependencies.autoload_paths << 'lib'
+ActiveSupport::Dependencies.autoload_paths << 'ee/lib'
+ActiveSupport::XmlMini.backend = 'Nokogiri'
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/admin/admin_users_impersonation_tokens_spec.rb b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
index e16eae219a4..c7860bebb06 100644
--- a/spec/features/admin/admin_users_impersonation_tokens_spec.rb
+++ b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
@@ -12,6 +12,10 @@ describe 'Admin > Users > Impersonation Tokens', :js do
find(".settings-message")
end
+ def created_impersonation_token
+ find("#created-personal-access-token").value
+ end
+
before do
sign_in(admin)
end
@@ -39,6 +43,7 @@ describe 'Admin > Users > Impersonation Tokens', :js do
expect(active_impersonation_tokens).to have_text('api')
expect(active_impersonation_tokens).to have_text('read_user')
expect(PersonalAccessTokensFinder.new(impersonation: true).execute.count).to equal(1)
+ expect(created_impersonation_token).not_to be_empty
end
end
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index d32f33ca1e2..f7c7a257075 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -130,7 +130,7 @@ describe "Admin::Users" do
context 'with regex to match internal user email address set', :js do
before do
stub_application_setting(user_default_external: true)
- stub_application_setting(user_default_internal_regex: '.internal@')
+ stub_application_setting(user_default_internal_regex: '\.internal@')
visit new_admin_user_path
end
@@ -169,6 +169,22 @@ describe "Admin::Users" do
expects_warning_to_be_hidden
end
+
+ it 'creates an internal user' do
+ user_name = 'tester1'
+ fill_in 'user_email', with: 'test.internal@domain.ch'
+ fill_in 'user_name', with: 'tester1 name'
+ fill_in 'user_username', with: user_name
+
+ expects_external_to_be_unchecked
+ expects_warning_to_be_shown
+
+ click_button 'Create user'
+
+ new_user = User.find_by(username: user_name)
+
+ expect(new_user.external).to be_falsy
+ end
end
end
end
diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb
index bd4c00d97b1..5fa1a26f1a6 100644
--- a/spec/features/atom/dashboard_issues_spec.rb
+++ b/spec/features/atom/dashboard_issues_spec.rb
@@ -25,35 +25,35 @@ describe "Dashboard Issues Feed" do
it "renders atom feed via personal access token" do
personal_access_token = create(:personal_access_token, user: user)
- visit issues_dashboard_path(:atom, private_token: personal_access_token.token, assignee_id: user.id)
+ visit issues_dashboard_path(:atom, private_token: personal_access_token.token, assignee_username: user.username)
expect(response_headers['Content-Type']).to have_content('application/atom+xml')
expect(body).to have_selector('title', text: "#{user.name} issues")
end
it "renders atom feed via feed token" do
- visit issues_dashboard_path(:atom, feed_token: user.feed_token, assignee_id: user.id)
+ visit issues_dashboard_path(:atom, feed_token: user.feed_token, assignee_username: user.username)
expect(response_headers['Content-Type']).to have_content('application/atom+xml')
expect(body).to have_selector('title', text: "#{user.name} issues")
end
it "renders atom feed with url parameters" do
- visit issues_dashboard_path(:atom, feed_token: user.feed_token, state: 'opened', assignee_id: user.id)
+ visit issues_dashboard_path(:atom, feed_token: user.feed_token, state: 'opened', assignee_username: user.username)
link = find('link[type="application/atom+xml"]')
params = CGI.parse(URI.parse(link[:href]).query)
expect(params).to include('feed_token' => [user.feed_token])
expect(params).to include('state' => ['opened'])
- expect(params).to include('assignee_id' => [user.id.to_s])
+ expect(params).to include('assignee_username' => [user.username.to_s])
end
context "issue with basic fields" do
let!(:issue2) { create(:issue, author: user, assignees: [assignee], project: project2, description: 'test desc') }
it "renders issue fields" do
- visit issues_dashboard_path(:atom, feed_token: user.feed_token, assignee_id: assignee.id)
+ visit issues_dashboard_path(:atom, feed_token: user.feed_token, assignee_username: assignee.username)
entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue2.title}')]")
@@ -76,7 +76,7 @@ describe "Dashboard Issues Feed" do
end
it "renders issue label and milestone info" do
- visit issues_dashboard_path(:atom, feed_token: user.feed_token, assignee_id: assignee.id)
+ visit issues_dashboard_path(:atom, feed_token: user.feed_token, assignee_username: assignee.username)
entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue1.title}')]")
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/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb
index 615223a2a88..2cdd3f55b50 100644
--- a/spec/features/boards/modal_filter_spec.rb
+++ b/spec/features/boards/modal_filter_spec.rb
@@ -106,7 +106,7 @@ describe 'Issue Boards add issue modal filtering', :js do
it 'filters by unassigned' do
set_filter('assignee')
- click_filter_link('No Assignee')
+ click_filter_link('None')
submit_filter
page.within('.add-issues-modal') do
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index 0bf1ecbc433..164442a47f5 100644
--- a/spec/features/boards/new_issue_spec.rb
+++ b/spec/features/boards/new_issue_spec.rb
@@ -94,8 +94,14 @@ describe 'Issue Boards new issue', :js do
wait_for_requests
end
- it 'does not display new issue button' do
- expect(page).to have_selector('.issue-count-badge-add-button', count: 0)
+ it 'displays new issue button in open list' do
+ expect(first('.board')).to have_selector('.issue-count-badge-add-button', count: 1)
+ end
+
+ it 'does not display new issue button in label list' do
+ page.within('.board:nth-child(2)') do
+ expect(page).not_to have_selector('.issue-count-badge-add-button')
+ end
end
end
end
diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb
index aa3ca8923ff..8cb9b57a049 100644
--- a/spec/features/calendar_spec.rb
+++ b/spec/features/calendar_spec.rb
@@ -64,7 +64,7 @@ describe 'Contributions Calendar', :js do
end
def selected_day_activities(visible: true)
- find('.tab-pane#activity .user-calendar-activities', visible: visible).text
+ find('#js-overview .user-calendar-activities', visible: visible).text
end
before do
@@ -74,16 +74,16 @@ describe 'Contributions Calendar', :js do
describe 'calendar day selection' do
before do
visit user.username
- page.find('.js-activity-tab a').click
+ page.find('.js-overview-tab a').click
wait_for_requests
end
it 'displays calendar' do
- expect(find('.tab-pane#activity')).to have_css('.js-contrib-calendar')
+ expect(find('#js-overview')).to have_css('.js-contrib-calendar')
end
describe 'select calendar day' do
- let(:cells) { page.all('.tab-pane#activity .user-contrib-cell') }
+ let(:cells) { page.all('#js-overview .user-contrib-cell') }
before do
cells[0].click
@@ -109,7 +109,7 @@ describe 'Contributions Calendar', :js do
describe 'deselect calendar day' do
before do
cells[0].click
- page.find('.js-activity-tab a').click
+ page.find('.js-overview-tab a').click
wait_for_requests
end
@@ -124,7 +124,7 @@ describe 'Contributions Calendar', :js do
shared_context 'visit user page' do
before do
visit user.username
- page.find('.js-activity-tab a').click
+ page.find('.js-overview-tab a').click
wait_for_requests
end
end
@@ -133,12 +133,12 @@ describe 'Contributions Calendar', :js do
include_context 'visit user page'
it 'displays calendar activity square color for 1 contribution' do
- expect(find('.tab-pane#activity')).to have_selector(get_cell_color_selector(contribution_count), count: 1)
+ expect(find('#js-overview')).to have_selector(get_cell_color_selector(contribution_count), count: 1)
end
it 'displays calendar activity square on the correct date' do
today = Date.today.strftime(date_format)
- expect(find('.tab-pane#activity')).to have_selector(get_cell_date_selector(contribution_count, today), count: 1)
+ expect(find('#js-overview')).to have_selector(get_cell_date_selector(contribution_count, today), count: 1)
end
end
@@ -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('#js-overview .overview-content-list .event-target-title')).to have_content issue_title
end
end
end
@@ -185,17 +185,17 @@ describe 'Contributions Calendar', :js do
include_context 'visit user page'
it 'displays calendar activity squares for both days' do
- expect(find('.tab-pane#activity')).to have_selector(get_cell_color_selector(1), count: 2)
+ expect(find('#js-overview')).to have_selector(get_cell_color_selector(1), count: 2)
end
it 'displays calendar activity square for yesterday' do
yesterday = Date.yesterday.strftime(date_format)
- expect(find('.tab-pane#activity')).to have_selector(get_cell_date_selector(1, yesterday), count: 1)
+ expect(find('#js-overview')).to have_selector(get_cell_date_selector(1, yesterday), count: 1)
end
it 'displays calendar activity square for today' do
today = Date.today.strftime(date_format)
- expect(find('.tab-pane#activity')).to have_selector(get_cell_date_selector(1, today), count: 1)
+ expect(find('#js-overview')).to have_selector(get_cell_date_selector(1, today), count: 1)
end
end
end
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index 8989b2051bb..5c6c1c4fd15 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -114,33 +114,6 @@ describe 'Commits' do
expect(page).to have_content 'canceled'
end
end
-
- describe '.gitlab-ci.yml not found warning' do
- context 'ci builds enabled' do
- it "does not show warning" do
- visit pipeline_path(pipeline)
- expect(page).not_to have_content '.gitlab-ci.yml not found in this commit'
- end
-
- it 'shows warning' do
- stub_ci_pipeline_yaml_file(nil)
- visit pipeline_path(pipeline)
- expect(page).to have_content '.gitlab-ci.yml not found in this commit'
- end
- end
-
- context 'ci builds disabled' do
- before do
- stub_ci_builds_disabled
- stub_ci_pipeline_yaml_file(nil)
- visit pipeline_path(pipeline)
- end
-
- it 'does not show warning' do
- expect(page).not_to have_content '.gitlab-ci.yml not found in this commit'
- end
- end
- end
end
context "when logged as reporter" do
@@ -182,6 +155,39 @@ describe 'Commits' do
end
end
end
+
+ describe '.gitlab-ci.yml not found warning' do
+ before do
+ project.add_reporter(user)
+ end
+
+ context 'ci builds enabled' do
+ it 'does not show warning' do
+ visit pipeline_path(pipeline)
+
+ expect(page).not_to have_content '.gitlab-ci.yml not found in this commit'
+ end
+
+ it 'shows warning' do
+ stub_ci_pipeline_yaml_file(nil)
+
+ visit pipeline_path(pipeline)
+
+ expect(page).to have_content '.gitlab-ci.yml not found in this commit'
+ end
+ end
+
+ context 'ci builds disabled' do
+ it 'does not show warning' do
+ stub_ci_builds_disabled
+ stub_ci_pipeline_yaml_file(nil)
+
+ visit pipeline_path(pipeline)
+
+ expect(page).not_to have_content '.gitlab-ci.yml not found in this commit'
+ end
+ end
+ end
end
context 'viewing commits for a branch' do
diff --git a/spec/features/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/issuables_counter_spec.rb b/spec/features/dashboard/issuables_counter_spec.rb
index b431f72fcc9..fbc2e5cc3d3 100644
--- a/spec/features/dashboard/issuables_counter_spec.rb
+++ b/spec/features/dashboard/issuables_counter_spec.rb
@@ -45,11 +45,11 @@ describe 'Navigation bar counter', :use_clean_rails_memory_store_caching do
end
def issues_path
- issues_dashboard_path(assignee_id: user.id)
+ issues_dashboard_path(assignee_username: user.username)
end
def merge_requests_path
- merge_requests_dashboard_path(assignee_id: user.id)
+ merge_requests_dashboard_path(assignee_username: user.username)
end
def expect_counters(issuable_type, count)
diff --git a/spec/features/dashboard/issues_filter_spec.rb b/spec/features/dashboard/issues_filter_spec.rb
index 95e2610dd4a..c0434f767bb 100644
--- a/spec/features/dashboard/issues_filter_spec.rb
+++ b/spec/features/dashboard/issues_filter_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe 'Dashboard Issues filtering', :js do
include Spec::Support::Helpers::Features::SortingHelpers
+ include FilteredSearchHelpers
let(:user) { create(:user) }
let(:project) { create(:project) }
@@ -25,27 +26,21 @@ describe 'Dashboard Issues filtering', :js do
context 'filtering by milestone' do
it 'shows all issues with no milestone' do
- show_milestone_dropdown
-
- click_link 'No Milestone'
+ input_filtered_search("milestone:none")
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_selector('.issue', count: 1)
end
it 'shows all issues with the selected milestone' do
- show_milestone_dropdown
-
- page.within '.dropdown-content' do
- click_link milestone.title
- end
+ input_filtered_search("milestone:%\"#{milestone.title}\"")
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_selector('.issue', count: 1)
end
it 'updates atom feed link' do
- visit_issues(milestone_title: '', assignee_id: user.id)
+ visit_issues(milestone_title: '', assignee_username: user.username)
link = find('.nav-controls a[title="Subscribe to RSS feed"]')
params = CGI.parse(URI.parse(link[:href]).query)
@@ -54,10 +49,10 @@ describe 'Dashboard Issues filtering', :js do
expect(params).to include('feed_token' => [user.feed_token])
expect(params).to include('milestone_title' => [''])
- expect(params).to include('assignee_id' => [user.id.to_s])
+ expect(params).to include('assignee_username' => [user.username.to_s])
expect(auto_discovery_params).to include('feed_token' => [user.feed_token])
expect(auto_discovery_params).to include('milestone_title' => [''])
- expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s])
+ expect(auto_discovery_params).to include('assignee_username' => [user.username.to_s])
end
end
@@ -66,10 +61,7 @@ describe 'Dashboard Issues filtering', :js do
let!(:label_link) { create(:label_link, label: label, target: issue) }
it 'shows all issues with the selected label' do
- page.within '.labels-filter' do
- find('.dropdown').click
- click_link label.title
- end
+ input_filtered_search("label:~#{label.title}")
page.within 'ul.content-list' do
expect(page).to have_content issue.title
@@ -80,12 +72,12 @@ describe 'Dashboard Issues filtering', :js do
context 'sorting' do
before do
- visit_issues(assignee_id: user.id)
+ visit_issues(assignee_username: user.username)
end
it 'remembers last sorting value' do
sort_by('Created date')
- visit_issues(assignee_id: user.id)
+ visit_issues(assignee_username: user.username)
expect(find('.issues-filters')).to have_content('Created date')
end
@@ -98,11 +90,6 @@ describe 'Dashboard Issues filtering', :js do
end
end
- def show_milestone_dropdown
- click_button 'Milestone'
- expect(page).to have_selector('.dropdown-content', visible: true)
- end
-
def visit_issues(*args)
visit issues_dashboard_path(*args)
end
diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb
index 4ae062f242a..9957bec0f0b 100644
--- a/spec/features/dashboard/issues_spec.rb
+++ b/spec/features/dashboard/issues_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
RSpec.describe 'Dashboard Issues' do
+ include FilteredSearchHelpers
+
let(:current_user) { create :user }
let(:user) { current_user } # Shared examples depend on this being available
let!(:public_project) { create(:project, :public) }
@@ -14,7 +16,7 @@ RSpec.describe 'Dashboard Issues' do
before do
[project, project_with_issues_disabled].each { |project| project.add_maintainer(current_user) }
sign_in(current_user)
- visit issues_dashboard_path(assignee_id: current_user.id)
+ visit issues_dashboard_path(assignee_username: current_user.username)
end
describe 'issues' do
@@ -24,26 +26,9 @@ RSpec.describe 'Dashboard Issues' do
expect(page).not_to have_content(other_issue.title)
end
- it 'shows checkmark when unassigned is selected for assignee', :js do
- find('.js-assignee-search').click
- find('li', text: 'Unassigned').click
- find('.js-assignee-search').click
-
- expect(find('li[data-user-id="0"] a.is-active')).to be_visible
- end
-
it 'shows issues when current user is author', :js do
- execute_script("document.querySelector('#assignee_id').value=''")
- find('.js-author-search', match: :first).click
-
- expect(find('li[data-user-id="null"] a.is-active')).to be_visible
-
- find('.dropdown-menu-author li a', match: :first, text: current_user.to_reference).click
- find('.js-author-search', match: :first).click
-
- page.within '.dropdown-menu-user' do
- expect(find('.dropdown-menu-author li a.is-active', match: :first, text: current_user.to_reference)).to be_visible
- end
+ reset_filters
+ input_filtered_search("author:#{current_user.to_reference}")
expect(page).to have_content(authored_issue.title)
expect(page).to have_content(authored_issue_on_public_project.title)
@@ -53,7 +38,7 @@ RSpec.describe 'Dashboard Issues' do
it 'state filter tabs work' do
find('#state-closed').click
- expect(page).to have_current_path(issues_dashboard_url(assignee_id: current_user.id, state: 'closed'), url: true)
+ expect(page).to have_current_path(issues_dashboard_url(assignee_username: current_user.username, state: 'closed'), url: true)
end
it_behaves_like "it has an RSS button with current_user's feed token"
diff --git a/spec/features/dashboard/label_filter_spec.rb b/spec/features/dashboard/label_filter_spec.rb
index 6802974c2ee..2d4659d380f 100644
--- a/spec/features/dashboard/label_filter_spec.rb
+++ b/spec/features/dashboard/label_filter_spec.rb
@@ -1,6 +1,11 @@
require 'spec_helper'
describe 'Dashboard > label filter', :js do
+ include FilteredSearchHelpers
+
+ let(:filtered_search) { find('.filtered-search') }
+ let(:filter_dropdown) { find("#js-dropdown-label .filter-dropdown") }
+
let(:user) { create(:user) }
let(:project) { create(:project, name: 'test', namespace: user.namespace) }
let(:project2) { create(:project, name: 'test2', path: 'test2', namespace: user.namespace) }
@@ -13,17 +18,15 @@ describe 'Dashboard > label filter', :js do
sign_in(user)
visit issues_dashboard_path
+
+ init_label_search
end
context 'duplicate labels' do
it 'removes duplicate labels' do
- page.within('.labels-filter') do
- click_button 'Label'
- end
+ filtered_search.send_keys('bu')
- page.within('.dropdown-menu-labels') do
- expect(page).to have_selector('.dropdown-content a', text: 'bug', count: 1)
- end
+ expect(filter_dropdown).to have_selector('.filter-dropdown-item', text: 'bug', count: 1)
end
end
end
diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb
index f51142f5790..282bf542e77 100644
--- a/spec/features/dashboard/merge_requests_spec.rb
+++ b/spec/features/dashboard/merge_requests_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe 'Dashboard Merge Requests' do
include Spec::Support::Helpers::Features::SortingHelpers
- include FilterItemSelectHelper
+ include FilteredSearchHelpers
include ProjectForksHelper
let(:current_user) { create :user }
@@ -36,7 +36,7 @@ describe 'Dashboard Merge Requests' do
context 'no merge requests exist' do
it 'shows an empty state' do
- visit merge_requests_dashboard_path(assignee_id: current_user.id)
+ visit merge_requests_dashboard_path(assignee_username: current_user.username)
expect(page).to have_selector('.empty-state')
end
@@ -79,7 +79,7 @@ describe 'Dashboard Merge Requests' do
end
before do
- visit merge_requests_dashboard_path(assignee_id: current_user.id)
+ visit merge_requests_dashboard_path(assignee_username: current_user.username)
end
it 'shows assigned merge requests' do
@@ -92,8 +92,8 @@ describe 'Dashboard Merge Requests' do
end
it 'shows authored merge requests', :js do
- filter_item_select('Any Assignee', '.js-assignee-search')
- filter_item_select(current_user.to_reference, '.js-author-search')
+ reset_filters
+ input_filtered_search("author:#{current_user.to_reference}")
expect(page).to have_content(authored_merge_request.title)
expect(page).to have_content(authored_merge_request_from_fork.title)
@@ -104,8 +104,7 @@ describe 'Dashboard Merge Requests' do
end
it 'shows error message without filter', :js do
- filter_item_select('Any Assignee', '.js-assignee-search')
- filter_item_select('Any Author', '.js-author-search')
+ reset_filters
expect(page).to have_content('Please select at least one filter to see results')
end
@@ -113,7 +112,7 @@ describe 'Dashboard Merge Requests' do
it 'shows sorted merge requests' do
sort_by('Created date')
- visit merge_requests_dashboard_path(assignee_id: current_user.id)
+ visit merge_requests_dashboard_path(assignee_username: current_user.username)
expect(find('.issues-filters')).to have_content('Created date')
end
diff --git a/spec/features/dashboard/milestone_filter_spec.rb b/spec/features/dashboard/milestone_filter_spec.rb
deleted file mode 100644
index 00373050aeb..00000000000
--- a/spec/features/dashboard/milestone_filter_spec.rb
+++ /dev/null
@@ -1,77 +0,0 @@
-require 'spec_helper'
-
-describe 'Dashboard > milestone filter', :js do
- include FilterItemSelectHelper
-
- let(:user) { create(:user) }
- let(:project) { create(:project, name: 'test', namespace: user.namespace) }
- let(:milestone) { create(:milestone, title: 'v1.0', project: project) }
- let(:milestone2) { create(:milestone, title: 'v2.0', project: project) }
- let!(:issue) { create :issue, author: user, project: project, milestone: milestone }
- let!(:issue2) { create :issue, author: user, project: project, milestone: milestone2 }
-
- dropdown_toggle_button = '.js-milestone-select'
-
- before do
- sign_in(user)
- end
-
- context 'default state' do
- it 'shows issues with Any Milestone' do
- visit issues_dashboard_path(author_id: user.id)
-
- page.all('.issue-info').each do |issue_info|
- expect(issue_info.text).to match(/v\d.0/)
- end
- end
- end
-
- context 'filtering by milestone' do
- before do
- visit issues_dashboard_path(author_id: user.id)
- filter_item_select('v1.0', dropdown_toggle_button)
- find(dropdown_toggle_button).click
- wait_for_requests
- end
-
- it 'shows issues with Milestone v1.0' do
- expect(find('.issues-list')).to have_selector('.issue', count: 1)
- expect(find('.milestone-filter .dropdown-content')).to have_selector('a.is-active', count: 1)
- end
-
- it 'should not change active Milestone unless clicked' do
- page.within '.milestone-filter' do
- expect(find('.dropdown-content')).to have_selector('a.is-active', count: 1)
-
- find('.dropdown-menu-close').click
-
- expect(page).not_to have_selector('.dropdown.open')
-
- find(dropdown_toggle_button).click
-
- expect(find('.dropdown-content')).to have_selector('a.is-active', count: 1)
- expect(find('.dropdown-content a.is-active')).to have_content('v1.0')
- end
- end
- end
-
- context 'with milestone filter in URL' do
- before do
- visit issues_dashboard_path(author_id: user.id, milestone_title: milestone.title)
- find(dropdown_toggle_button).click
- wait_for_requests
- end
-
- it 'has milestone selected' do
- expect(find('.milestone-filter .dropdown-content')).to have_css('.is-active', text: milestone.title)
- end
-
- it 'removes milestone filter from URL after clicking "Any Milestone"' do
- expect(current_url).to include("milestone_title=#{milestone.title}")
-
- find('.milestone-filter .dropdown-content li', text: 'Any Milestone').click
-
- expect(current_url).not_to include('milestone_title')
- end
- 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/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/milestone_spec.rb b/spec/features/groups/milestone_spec.rb
index e8ca6a6714f..174840794ed 100644
--- a/spec/features/groups/milestone_spec.rb
+++ b/spec/features/groups/milestone_spec.rb
@@ -95,9 +95,9 @@ describe 'Group milestones' do
end
it 'counts milestones correctly' do
- expect(find('.top-area .active .badge').text).to eq("2")
- expect(find('.top-area .closed .badge').text).to eq("2")
- expect(find('.top-area .all .badge').text).to eq("4")
+ expect(find('.top-area .active .badge').text).to eq("3")
+ expect(find('.top-area .closed .badge').text).to eq("3")
+ expect(find('.top-area .all .badge').text).to eq("6")
end
it 'lists legacy group milestones and group milestones' do
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_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
index d011d2545bb..e910fb54d23 100644
--- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
@@ -156,13 +156,21 @@ describe 'Dropdown assignee', :js do
expect_filtered_search_input_empty
end
- it 'selects `no assignee`' do
- find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click
+ it 'selects `None`' do
+ find('#js-dropdown-assignee .filter-dropdown-item', text: 'None').click
expect(page).to have_css(js_dropdown_assignee, visible: false)
expect_tokens([assignee_token('none')])
expect_filtered_search_input_empty
end
+
+ it 'selects `Any`' do
+ find('#js-dropdown-assignee .filter-dropdown-item', text: 'Any').click
+
+ expect(page).to have_css(js_dropdown_assignee, visible: false)
+ expect_tokens([assignee_token('any')])
+ expect_filtered_search_input_empty
+ end
end
describe 'selecting from dropdown without Ajax call' do
diff --git a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
index be229e8aa7d..97dd0afd002 100644
--- a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
@@ -20,7 +20,7 @@ describe 'Dropdown emoji', :js do
end
def dropdown_emoji_size
- page.all('#js-dropdown-my-reaction .filter-dropdown .filter-dropdown-item').size
+ all('gl-emoji[data-name]').size
end
def click_emoji(text)
@@ -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/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb
index 6ac7ccd00f7..1e1dd5691ab 100644
--- a/spec/features/issues/filtered_search/visual_tokens_spec.rb
+++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb
@@ -118,7 +118,7 @@ describe 'Visual tokens', :js do
describe 'selecting static option from dropdown' do
before do
- find("#js-dropdown-assignee").find('.filter-dropdown-item', text: 'No Assignee').click
+ find("#js-dropdown-assignee").find('.filter-dropdown-item', text: 'None').click
end
it 'changes value in visual token' do
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index 1456a2f0375..f2e4c5779df 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -27,7 +27,7 @@ describe 'New/edit issue', :js do
before do
# Using `allow_any_instance_of`/`and_wrap_original`, `original` would
# somehow refer to the very block we defined to _wrap_ that method, instead of
- # the original method, resulting in infinite recurison when called.
+ # the original method, resulting in infinite recursion when called.
# This is likely a bug with helper modules included into dynamically generated view classes.
# To work around this, we have to hold on to and call to the original implementation manually.
original_issue_dropdown_options = FormHelper.instance_method(:issue_assignees_dropdown_options)
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index 08bf9bc7243..7c591dacce5 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -1,6 +1,10 @@
require 'rails_helper'
describe 'GFM autocomplete', :js do
+ let(:issue_xss_title) { 'This will execute alert<img src=x onerror=alert(2)&lt;img src=x onerror=alert(1)&gt;' }
+ let(:user_xss_title) { 'eve <img src=x onerror=alert(2)&lt;img src=x onerror=alert(1)&gt;' }
+
+ let(:user_xss) { create(:user, name: user_xss_title, username: 'xss.user') }
let(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
let(:project) { create(:project) }
let(:label) { create(:label, project: project, title: 'special+') }
@@ -9,13 +13,15 @@ describe 'GFM autocomplete', :js do
before do
project.add_maintainer(user)
+ project.add_maintainer(user_xss)
+
sign_in(user)
visit project_issue_path(project, issue)
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]}")
@@ -35,6 +41,32 @@ describe 'GFM autocomplete', :js do
expect(page).to have_selector('.atwho-container')
end
+ it 'opens autocomplete menu for Issues when field starts with text with item escaping HTML characters' do
+ create(:issue, project: project, title: issue_xss_title)
+
+ page.within '.timeline-content-form' do
+ find('#note-body').native.send_keys('#')
+ end
+
+ expect(page).to have_selector('.atwho-container')
+
+ page.within '.atwho-container #at-view-issues' do
+ expect(page.all('li').first.text).to include(issue_xss_title)
+ end
+ end
+
+ it 'opens autocomplete menu for Username when field starts with text with item escaping HTML characters' do
+ page.within '.timeline-content-form' do
+ find('#note-body').native.send_keys('@ev')
+ end
+
+ expect(page).to have_selector('.atwho-container')
+
+ page.within '.atwho-container #at-view-users' do
+ expect(find('li').text).to have_content(user_xss.username)
+ end
+ end
+
it 'doesnt open autocomplete menu character is prefixed with text' do
page.within '.timeline-content-form' do
find('#note-body').native.send_keys('testing')
@@ -92,7 +124,7 @@ describe 'GFM autocomplete', :js do
wait_for_requests
- expect(find('#at-view-64')).to have_selector('.cur:first-of-type')
+ expect(find('#at-view-users')).to have_selector('.cur:first-of-type')
end
it 'includes items for assignee dropdowns with non-ASCII characters in name' do
@@ -105,7 +137,7 @@ describe 'GFM autocomplete', :js do
wait_for_requests
- expect(find('#at-view-64')).to have_content(user.name)
+ expect(find('#at-view-users')).to have_content(user.name)
end
it 'selects the first item for non-assignee dropdowns if a query is entered' do
diff --git a/spec/features/issues/resource_label_events_spec.rb b/spec/features/issues/resource_label_events_spec.rb
index 40c452c991a..b0764db7751 100644
--- a/spec/features/issues/resource_label_events_spec.rb
+++ b/spec/features/issues/resource_label_events_spec.rb
@@ -7,6 +7,7 @@ describe 'List issue resource label events', :js do
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project, author: user) }
let!(:label) { create(:label, project: project, title: 'foo') }
+ let!(:user_status) { create(:user_status, user: user) }
context 'when user displays the issue' do
let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue, note: 'some note') }
@@ -23,6 +24,12 @@ describe 'List issue resource label events', :js do
expect(find("#note_#{event.discussion_id}")).to have_content 'added foo label'
end
end
+
+ it 'shows the user status on the system note for the label' do
+ page.within("#note_#{event.discussion_id}") do
+ expect(page).to show_user_status user_status
+ end
+ end
end
context 'when user adds label to the issue' do
diff --git a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
index 3dfcbc2fcb8..32bc851f00f 100644
--- a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
+++ b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
@@ -55,11 +55,11 @@ describe 'User creates branch and merge request on issue page', :js do
test_branch_name_checking(input_branch_name)
test_source_checking(input_source)
- # The button inside dropdown should be disabled if any errors occured.
+ # The button inside dropdown should be disabled if any errors occurred.
expect(page).to have_button('Create branch', disabled: true)
end
- # The top level button should be disabled if any errors occured.
+ # The top level button should be disabled if any errors occurred.
expect(page).to have_button('Create branch', disabled: true)
end
@@ -76,7 +76,7 @@ describe 'User creates branch and merge request on issue page', :js do
visit project_issue_path(project, issue)
- expect(page).to have_content('created branch 1-cherry-coloured-funk')
+ expect(page).to have_content("created merge request !1 to address this issue")
expect(page).to have_content('mentioned in merge request !1')
end
@@ -106,7 +106,7 @@ describe 'User creates branch and merge request on issue page', :js do
visit project_issue_path(project, issue)
- expect(page).to have_content('created branch custom-branch-name')
+ expect(page).to have_content("created merge request !1 to address this issue")
expect(page).to have_content('mentioned in merge request !1')
end
diff --git a/spec/features/issues/user_sorts_issues_spec.rb b/spec/features/issues/user_sorts_issues_spec.rb
index 7d261ec7dae..4771d2c6d28 100644
--- a/spec/features/issues/user_sorts_issues_spec.rb
+++ b/spec/features/issues/user_sorts_issues_spec.rb
@@ -26,7 +26,7 @@ describe "User sorts issues" do
click_link('Milestone')
end
- visit(issues_dashboard_path(assignee_id: user.id))
+ visit(issues_dashboard_path(assignee_username: user.username))
expect(find('.issues-filters a.is-active')).to have_content('Milestone')
diff --git a/spec/features/issues/user_uses_quick_actions_spec.rb b/spec/features/issues/user_uses_quick_actions_spec.rb
index 5926e442f24..27cffdc5f8b 100644
--- a/spec/features/issues/user_uses_quick_actions_spec.rb
+++ b/spec/features/issues/user_uses_quick_actions_spec.rb
@@ -303,5 +303,63 @@ describe 'Issues > User uses quick actions', :js do
end
end
end
+
+ describe 'create a merge request starting from an issue' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:issue) { create(:issue, project: project) }
+
+ def expect_mr_quickaction(success)
+ expect(page).to have_content 'Commands applied'
+
+ if success
+ expect(page).to have_content 'created merge request'
+ else
+ expect(page).not_to have_content 'created merge request'
+ end
+ end
+
+ it "doesn't create a merge request when the branch name is invalid" do
+ add_note("/create_merge_request invalid branch name")
+
+ wait_for_requests
+
+ expect_mr_quickaction(false)
+ end
+
+ it "doesn't create a merge request when a branch with that name already exists" do
+ add_note("/create_merge_request feature")
+
+ wait_for_requests
+
+ expect_mr_quickaction(false)
+ end
+
+ it 'creates a new merge request using issue iid and title as branch name when the branch name is empty' do
+ add_note("/create_merge_request")
+
+ wait_for_requests
+
+ expect_mr_quickaction(true)
+
+ created_mr = project.merge_requests.last
+ expect(created_mr.source_branch).to eq(issue.to_branch_name)
+
+ visit project_merge_request_path(project, created_mr)
+ expect(page).to have_content %{WIP: Resolve "#{issue.title}"}
+ end
+
+ it 'creates a merge request using the given branch name' do
+ branch_name = '1-feature'
+ add_note("/create_merge_request #{branch_name}")
+
+ expect_mr_quickaction(true)
+
+ created_mr = project.merge_requests.last
+ expect(created_mr.source_branch).to eq(branch_name)
+
+ visit project_merge_request_path(project, created_mr)
+ expect(page).to have_content %{WIP: Resolve "#{issue.title}"}
+ end
+ end
end
end
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_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 f744d7941f5..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,45 +1,88 @@
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) { merge_request.target_project }
- let(:merge_request) { create(:merge_request, :merged) }
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) { create(:merge_request, :merged, source_project: project) }
let(:environment) { create(:environment, project: project) }
let(:role) { :developer }
- let(:sha) { project.commit('master').id }
- let!(:deployment) { create(:deployment, environment: environment, sha: sha) }
+ 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!(: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(:pipeline) { create(:ci_pipeline, project: project) }
- let(:build) { create(:ci_build, pipeline: pipeline) }
+ 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') }
- let(:deployment) do
- create(:deployment, environment: environment, ref: merge_request.target_branch,
- sha: sha, deployable: build, on_stop: 'close_app')
- end
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
@@ -48,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 f15129759de..582be101399 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -40,21 +40,27 @@ describe 'Merge request > User sees merge widget', :js do
context 'view merge request' do
let!(:environment) { create(:environment, project: project) }
+ let(:sha) { project.commit(merge_request.source_branch).sha }
+ let(:pipeline) { create(:ci_pipeline_without_jobs, status: 'success', sha: sha, project: project, ref: merge_request.source_branch) }
+ let(:build) { create(:ci_build, :success, pipeline: pipeline) }
let!(:deployment) do
- create(:deployment, environment: environment,
- ref: 'feature',
- sha: merge_request.diff_head_sha)
+ create(:deployment, :succeed,
+ environment: environment,
+ ref: merge_request.source_branch,
+ deployable: build,
+ sha: sha)
end
before do
+ merge_request.update!(head_pipeline: pipeline)
visit project_merge_request_path(project, merge_request)
end
it 'shows environments link' do
wait_for_requests
- page.within('.mr-widget-heading') do
+ page.within('.js-pre-merge-deploy') do
expect(page).to have_content("Deployed to #{environment.name}")
expect(find('.js-deploy-url')[:href]).to include(environment.formatted_external_url)
end
@@ -174,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_requests/user_sorts_merge_requests_spec.rb b/spec/features/merge_requests/user_sorts_merge_requests_spec.rb
index 82cfe600d52..e163868e8e7 100644
--- a/spec/features/merge_requests/user_sorts_merge_requests_spec.rb
+++ b/spec/features/merge_requests/user_sorts_merge_requests_spec.rb
@@ -25,7 +25,7 @@ describe 'User sorts merge requests' do
click_link('Milestone')
end
- visit(merge_requests_dashboard_path(assignee_id: user.id))
+ visit(merge_requests_dashboard_path(assignee_username: user.username))
expect(find('.issues-filters a.is-active')).to have_content('Milestone')
@@ -41,7 +41,7 @@ describe 'User sorts merge requests' do
it 'fallbacks to issuable_sort cookie key when remembering the sorting option' do
set_cookie('issuable_sort', 'milestone')
- visit(merge_requests_dashboard_path(assignee_id: user.id))
+ visit(merge_requests_dashboard_path(assignee_username: user.username))
expect(find('.issues-filters a.is-active')).to have_content('Milestone')
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/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb
index 8461cd0027c..dee213a11d4 100644
--- a/spec/features/profiles/personal_access_tokens_spec.rb
+++ b/spec/features/profiles/personal_access_tokens_spec.rb
@@ -43,10 +43,12 @@ describe 'Profile > Personal Access Tokens', :js do
check "read_user"
click_on "Create personal access token"
+
expect(active_personal_access_tokens).to have_text(name)
expect(active_personal_access_tokens).to have_text('In')
expect(active_personal_access_tokens).to have_text('api')
expect(active_personal_access_tokens).to have_text('read_user')
+ expect(created_personal_access_token).not_to be_empty
end
context "when creation fails" do
@@ -57,6 +59,7 @@ describe 'Profile > Personal Access Tokens', :js do
expect { click_on "Create personal access token" }.not_to change { PersonalAccessToken.count }
expect(page).to have_content("Name cannot be nil")
+ expect(page).not_to have_selector("#created-personal-access-token")
end
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_files_spec.rb b/spec/features/projects/files/user_creates_files_spec.rb
index d4dda43c823..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)
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index c3902ecdd17..99a7fbb63bd 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
@@ -699,7 +719,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
context 'on mobile', :js do
let(:job) { create(:ci_build, pipeline: pipeline) }
- it 'renders collpased sidebar' do
+ it 'renders collapsed sidebar' do
page.current_window.resize_to(600, 800)
visit project_job_path(project, job)
@@ -718,7 +738,63 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
wait_for_requests
expect(page).to have_css('.js-job-sidebar.right-sidebar-expanded')
- expect(page).not_to have_css('.js-job-sidebar.right-sidebar-collpased')
+ expect(page).not_to have_css('.js-job-sidebar.right-sidebar-collapsed')
+ end
+ end
+
+ context 'stuck', :js do
+ before do
+ visit project_job_path(project, job)
+ wait_for_requests
+ end
+
+ context 'without active runners available' do
+ let(:runner) { create(:ci_runner, :instance, active: false) }
+ let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner: runner) }
+
+ it 'renders message about job being stuck because no runners are active' do
+ expect(page).to have_css('.js-stuck-no-active-runner')
+ expect(page).to have_content("This job is stuck, because you don't have any active runners that can run this job.")
+ end
+ end
+
+ context 'when available runners can not run specified tag' do
+ let(:runner) { create(:ci_runner, :instance, active: false) }
+ let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner: runner, tag_list: %w(docker linux)) }
+
+ it 'renders message about job being stuck because of no runners with the specified tags' do
+ expect(page).to have_css('.js-stuck-with-tags')
+ expect(page).to have_content("This job is stuck, because you don't have any active runners online with any of these tags assigned to them:")
+ end
+ end
+
+ context 'when runners are offline and build has tags' do
+ let(:runner) { create(:ci_runner, :instance, active: true) }
+ let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner: runner, tag_list: %w(docker linux)) }
+
+ it 'renders message about job being stuck because of no runners with the specified tags' do
+ expect(page).to have_css('.js-stuck-with-tags')
+ expect(page).to have_content("This job is stuck, because you don't have any active runners online with any of these tags assigned to them:")
+ end
+ end
+
+ context 'without any runners available' do
+ let(:job) { create(:ci_build, :pending, pipeline: pipeline) }
+
+ it 'renders message about job being stuck because not runners are available' do
+ expect(page).to have_css('.js-stuck-no-active-runner')
+ expect(page).to have_content("This job is stuck, because you don't have any active runners that can run this job.")
+ end
+ end
+
+ context 'without available runners online' do
+ let(:runner) { create(:ci_runner, :instance, active: true) }
+ let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner: runner) }
+
+ it 'renders message about job being stuck because runners are offline' do
+ expect(page).to have_css('.js-stuck-no-runners')
+ expect(page).to have_content("This job is stuck, because the project doesn't have any runners online assigned to it.")
+ end
end
end
end
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/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
index 2ce5ee0e87d..7ad7fec922a 100644
--- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
@@ -1,233 +1,223 @@
require 'spec_helper'
describe 'User updates wiki page' do
- shared_examples 'wiki page user update' do
- let(:user) { create(:user) }
+ let(:user) { create(:user) }
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ context 'when wiki is empty' do
before do
- project.add_maintainer(user)
- sign_in(user)
+ visit(project_wikis_path(project))
+ click_link "Create your first page"
end
- context 'when wiki is empty' do
- before do
- visit(project_wikis_path(project))
- click_link "Create your first page"
- end
-
- context 'in a user namespace' do
- let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
+ context 'in a user namespace' do
+ let(:project) { create(:project, :wiki_repo) }
- it 'redirects back to the home edit page' do
- page.within(:css, '.wiki-form .form-actions') do
- click_on('Cancel')
- end
-
- expect(current_path).to eq project_wiki_path(project, :home)
+ it 'redirects back to the home edit page' do
+ page.within(:css, '.wiki-form .form-actions') do
+ click_on('Cancel')
end
- it 'updates a page that has a path', :js do
- click_on('New page')
+ expect(current_path).to eq project_wiki_path(project, :home)
+ end
- page.within('#modal-new-wiki') do
- fill_in(:new_wiki_path, with: 'one/two/three-test')
- click_on('Create page')
- end
+ it 'updates a page that has a path', :js do
+ click_on('New page')
- page.within '.wiki-form' do
- fill_in(:wiki_content, with: 'wiki content')
- click_on('Create page')
- end
+ page.within('#modal-new-wiki') do
+ fill_in(:new_wiki_path, with: 'one/two/three-test')
+ click_on('Create page')
+ end
- expect(current_path).to include('one/two/three-test')
- expect(find('.wiki-pages')).to have_content('Three')
+ page.within '.wiki-form' do
+ fill_in(:wiki_content, with: 'wiki content')
+ click_on('Create page')
+ end
- first(:link, text: 'Three').click
+ expect(current_path).to include('one/two/three-test')
+ expect(find('.wiki-pages')).to have_content('Three')
- expect(find('.nav-text')).to have_content('Three')
+ first(:link, text: 'Three').click
- click_on('Edit')
+ expect(find('.nav-text')).to have_content('Three')
- expect(current_path).to include('one/two/three-test')
- expect(page).to have_content('Edit Page')
+ click_on('Edit')
- fill_in('Content', with: 'Updated Wiki Content')
- click_on('Save changes')
+ expect(current_path).to include('one/two/three-test')
+ expect(page).to have_content('Edit Page')
- expect(page).to have_content('Updated Wiki Content')
- end
+ fill_in('Content', with: 'Updated Wiki Content')
+ click_on('Save changes')
- it_behaves_like 'wiki file attachments'
+ expect(page).to have_content('Updated Wiki Content')
end
- end
- context 'when wiki is not empty' do
- let(:project_wiki) { create(:project_wiki, project: project, user: project.creator) }
- let!(:wiki_page) { create(:wiki_page, wiki: project_wiki, attrs: { title: 'home', content: 'Home page' }) }
+ it_behaves_like 'wiki file attachments'
+ end
+ end
- before do
- visit(project_wikis_path(project))
+ context 'when wiki is not empty' do
+ let(:project_wiki) { create(:project_wiki, project: project, user: project.creator) }
+ let!(:wiki_page) { create(:wiki_page, wiki: project_wiki, attrs: { title: 'home', content: 'Home page' }) }
- click_link('Edit')
- end
+ before do
+ visit(project_wikis_path(project))
- context 'in a user namespace' do
- let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
+ click_link('Edit')
+ end
- it 'updates a page' do
- # Commit message field should have correct value.
- expect(page).to have_field('wiki[message]', with: 'Update home')
+ context 'in a user namespace' do
+ let(:project) { create(:project, :wiki_repo) }
- fill_in(:wiki_content, with: 'My awesome wiki!')
- click_button('Save changes')
+ it 'updates a page' do
+ # Commit message field should have correct value.
+ expect(page).to have_field('wiki[message]', with: 'Update home')
- expect(page).to have_content('Home')
- expect(page).to have_content("Last edited by #{user.name}")
- expect(page).to have_content('My awesome wiki!')
- end
+ fill_in(:wiki_content, with: 'My awesome wiki!')
+ click_button('Save changes')
- it 'shows a validation error message' do
- fill_in(:wiki_content, with: '')
- click_button('Save changes')
+ expect(page).to have_content('Home')
+ expect(page).to have_content("Last edited by #{user.name}")
+ expect(page).to have_content('My awesome wiki!')
+ end
- expect(page).to have_selector('.wiki-form')
- expect(page).to have_content('Edit Page')
- expect(page).to have_content('The form contains the following error:')
- expect(page).to have_content("Content can't be blank")
- expect(find('textarea#wiki_content').value).to eq('')
- end
+ it 'shows a validation error message' do
+ fill_in(:wiki_content, with: '')
+ click_button('Save changes')
- it 'shows the emoji autocompletion dropdown', :js do
- find('#wiki_content').native.send_keys('')
- fill_in(:wiki_content, with: ':')
+ expect(page).to have_selector('.wiki-form')
+ expect(page).to have_content('Edit Page')
+ expect(page).to have_content('The form contains the following error:')
+ expect(page).to have_content("Content can't be blank")
+ expect(find('textarea#wiki_content').value).to eq('')
+ end
- expect(page).to have_selector('.atwho-view')
- end
+ it 'shows the emoji autocompletion dropdown', :js do
+ find('#wiki_content').native.send_keys('')
+ fill_in(:wiki_content, with: ':')
- it 'shows the error message' do
- wiki_page.update(content: 'Update')
+ expect(page).to have_selector('.atwho-view')
+ end
- click_button('Save changes')
+ it 'shows the error message' do
+ wiki_page.update(content: 'Update')
- expect(page).to have_content('Someone edited the page the same time you did.')
- end
+ click_button('Save changes')
- it 'updates a page' do
- fill_in('Content', with: 'Updated Wiki Content')
- click_on('Save changes')
+ expect(page).to have_content('Someone edited the page the same time you did.')
+ end
- expect(page).to have_content('Updated Wiki Content')
- end
+ it 'updates a page' do
+ fill_in('Content', with: 'Updated Wiki Content')
+ click_on('Save changes')
- it 'cancels editing of a page' do
- page.within(:css, '.wiki-form .form-actions') do
- click_on('Cancel')
- end
+ expect(page).to have_content('Updated Wiki Content')
+ end
- expect(current_path).to eq(project_wiki_path(project, wiki_page))
+ it 'cancels editing of a page' do
+ page.within(:css, '.wiki-form .form-actions') do
+ click_on('Cancel')
end
- it_behaves_like 'wiki file attachments'
+ expect(current_path).to eq(project_wiki_path(project, wiki_page))
end
- context 'in a group namespace' do
- let(:project) { create(:project, :wiki_repo, namespace: create(:group, :public)) }
+ it_behaves_like 'wiki file attachments'
+ end
- it 'updates a page' do
- # Commit message field should have correct value.
- expect(page).to have_field('wiki[message]', with: 'Update home')
+ context 'in a group namespace' do
+ let(:project) { create(:project, :wiki_repo, namespace: create(:group, :public)) }
- fill_in(:wiki_content, with: 'My awesome wiki!')
+ it 'updates a page' do
+ # Commit message field should have correct value.
+ expect(page).to have_field('wiki[message]', with: 'Update home')
- click_button('Save changes')
+ fill_in(:wiki_content, with: 'My awesome wiki!')
- expect(page).to have_content('Home')
- expect(page).to have_content("Last edited by #{user.name}")
- expect(page).to have_content('My awesome wiki!')
- end
+ click_button('Save changes')
- it_behaves_like 'wiki file attachments'
+ expect(page).to have_content('Home')
+ expect(page).to have_content("Last edited by #{user.name}")
+ expect(page).to have_content('My awesome wiki!')
end
- end
- context 'when the page is in a subdir' do
- let!(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
- let(:project_wiki) { create(:project_wiki, project: project, user: project.creator) }
- let(:page_name) { 'page_name' }
- let(:page_dir) { "foo/bar/#{page_name}" }
- let!(:wiki_page) { create(:wiki_page, wiki: project_wiki, attrs: { title: page_dir, content: 'Home page' }) }
+ it_behaves_like 'wiki file attachments'
+ end
+ end
- before do
- visit(project_wiki_edit_path(project, wiki_page))
- end
+ context 'when the page is in a subdir' do
+ let!(:project) { create(:project, :wiki_repo) }
+ let(:project_wiki) { create(:project_wiki, project: project, user: project.creator) }
+ let(:page_name) { 'page_name' }
+ let(:page_dir) { "foo/bar/#{page_name}" }
+ let!(:wiki_page) { create(:wiki_page, wiki: project_wiki, attrs: { title: page_dir, content: 'Home page' }) }
- it 'moves the page to the root folder', :skip_gitaly_mock do
- fill_in(:wiki_title, with: "/#{page_name}")
+ before do
+ visit(project_wiki_edit_path(project, wiki_page))
+ end
- click_button('Save changes')
+ it 'moves the page to the root folder' do
+ fill_in(:wiki_title, with: "/#{page_name}")
- expect(current_path).to eq(project_wiki_path(project, page_name))
- end
+ click_button('Save changes')
- it 'moves the page to other dir' do
- new_page_dir = "foo1/bar1/#{page_name}"
+ expect(current_path).to eq(project_wiki_path(project, page_name))
+ end
- fill_in(:wiki_title, with: new_page_dir)
+ it 'moves the page to other dir' do
+ new_page_dir = "foo1/bar1/#{page_name}"
- click_button('Save changes')
+ fill_in(:wiki_title, with: new_page_dir)
- expect(current_path).to eq(project_wiki_path(project, new_page_dir))
- end
+ click_button('Save changes')
- it 'remains in the same place if title has not changed' do
- original_path = project_wiki_path(project, wiki_page)
+ expect(current_path).to eq(project_wiki_path(project, new_page_dir))
+ end
- fill_in(:wiki_title, with: page_name)
+ it 'remains in the same place if title has not changed' do
+ original_path = project_wiki_path(project, wiki_page)
- click_button('Save changes')
+ fill_in(:wiki_title, with: page_name)
- expect(current_path).to eq(original_path)
- end
+ click_button('Save changes')
- it 'can be moved to a different dir with a different name' do
- new_page_dir = "foo1/bar1/new_page_name"
+ expect(current_path).to eq(original_path)
+ end
- fill_in(:wiki_title, with: new_page_dir)
+ it 'can be moved to a different dir with a different name' do
+ new_page_dir = "foo1/bar1/new_page_name"
- click_button('Save changes')
+ fill_in(:wiki_title, with: new_page_dir)
- expect(current_path).to eq(project_wiki_path(project, new_page_dir))
- end
+ click_button('Save changes')
- it 'can be renamed and moved to the root folder' do
- new_name = 'new_page_name'
+ expect(current_path).to eq(project_wiki_path(project, new_page_dir))
+ end
- fill_in(:wiki_title, with: "/#{new_name}")
+ it 'can be renamed and moved to the root folder' do
+ new_name = 'new_page_name'
- click_button('Save changes')
+ fill_in(:wiki_title, with: "/#{new_name}")
- expect(current_path).to eq(project_wiki_path(project, new_name))
- end
+ click_button('Save changes')
- it 'squishes the title before creating the page' do
- new_page_dir = " foo1 / bar1 / #{page_name} "
+ expect(current_path).to eq(project_wiki_path(project, new_name))
+ end
- fill_in(:wiki_title, with: new_page_dir)
+ it 'squishes the title before creating the page' do
+ new_page_dir = " foo1 / bar1 / #{page_name} "
- click_button('Save changes')
+ fill_in(:wiki_title, with: new_page_dir)
- expect(current_path).to eq(project_wiki_path(project, "foo1/bar1/#{page_name}"))
- end
+ click_button('Save changes')
- it_behaves_like 'wiki file attachments'
+ expect(current_path).to eq(project_wiki_path(project, "foo1/bar1/#{page_name}"))
end
- end
-
- context 'when Gitaly is enabled' do
- it_behaves_like 'wiki page user update'
- end
- context 'when Gitaly is disabled', :skip_gitaly_mock do
- it_behaves_like 'wiki page user update'
+ it_behaves_like 'wiki file attachments'
end
end
diff --git a/spec/features/projects/wiki/user_views_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
index 4b974a3ca10..3c93d71ab00 100644
--- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
@@ -1,174 +1,164 @@
require 'spec_helper'
describe 'User views a wiki page' do
- shared_examples 'wiki page user view' do
- include WikiHelpers
-
- let(:user) { create(:user) }
- let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
- let(:path) { 'image.png' }
- let(:wiki_page) do
- create(:wiki_page,
- wiki: project.wiki,
- attrs: { title: 'home', content: "Look at this [image](#{path})\n\n ![alt text](#{path})" })
- end
+ include WikiHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
+ let(:path) { 'image.png' }
+ let(:wiki_page) do
+ create(:wiki_page,
+ wiki: project.wiki,
+ attrs: { title: 'home', content: "Look at this [image](#{path})\n\n ![alt text](#{path})" })
+ end
- before do
- project.add_maintainer(user)
- sign_in(user)
- end
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
- context 'when wiki is empty' do
- before do
- visit(project_wikis_path(project))
- click_link "Create your first page"
+ context 'when wiki is empty' do
+ before do
+ visit(project_wikis_path(project))
+ click_link "Create your first page"
- click_on('New page')
+ click_on('New page')
- page.within('#modal-new-wiki') do
- fill_in(:new_wiki_path, with: 'one/two/three-test')
- click_on('Create page')
- end
+ page.within('#modal-new-wiki') do
+ fill_in(:new_wiki_path, with: 'one/two/three-test')
+ click_on('Create page')
+ end
- page.within('.wiki-form') do
- fill_in(:wiki_content, with: 'wiki content')
- click_on('Create page')
- end
+ page.within('.wiki-form') do
+ fill_in(:wiki_content, with: 'wiki content')
+ click_on('Create page')
end
+ end
- it 'shows the history of a page that has a path', :js do
- expect(current_path).to include('one/two/three-test')
+ it 'shows the history of a page that has a path', :js do
+ expect(current_path).to include('one/two/three-test')
- first(:link, text: 'Three').click
- click_on('Page history')
+ first(:link, text: 'Three').click
+ click_on('Page history')
- expect(current_path).to include('one/two/three-test')
+ expect(current_path).to include('one/two/three-test')
- page.within(:css, '.nav-text') do
- expect(page).to have_content('History')
- end
+ page.within(:css, '.nav-text') do
+ expect(page).to have_content('History')
end
+ end
- it 'shows an old version of a page', :js do
- expect(current_path).to include('one/two/three-test')
- expect(find('.wiki-pages')).to have_content('Three')
-
- first(:link, text: 'Three').click
-
- expect(find('.nav-text')).to have_content('Three')
+ it 'shows an old version of a page', :js do
+ expect(current_path).to include('one/two/three-test')
+ expect(find('.wiki-pages')).to have_content('Three')
- click_on('Edit')
+ first(:link, text: 'Three').click
- expect(current_path).to include('one/two/three-test')
- expect(page).to have_content('Edit Page')
+ expect(find('.nav-text')).to have_content('Three')
- fill_in('Content', with: 'Updated Wiki Content')
+ click_on('Edit')
- click_on('Save changes')
- click_on('Page history')
+ expect(current_path).to include('one/two/three-test')
+ expect(page).to have_content('Edit Page')
- page.within(:css, '.nav-text') do
- expect(page).to have_content('History')
- end
+ fill_in('Content', with: 'Updated Wiki Content')
- find('a[href*="?version_id"]')
- end
- end
-
- context 'when a page does not have history' do
- before do
- visit(project_wiki_path(project, wiki_page))
- end
+ click_on('Save changes')
+ click_on('Page history')
- it 'shows all the pages' do
- expect(page).to have_content(user.name)
- expect(find('.wiki-pages')).to have_content(wiki_page.title.capitalize)
+ page.within(:css, '.nav-text') do
+ expect(page).to have_content('History')
end
- context 'shows a file stored in a page' do
- let(:path) { upload_file_to_wiki(project, user, 'dk.png') }
+ find('a[href*="?version_id"]')
+ end
+ end
- it do
- expect(page).to have_xpath("//img[@data-src='#{project.wiki.wiki_base_path}/#{path}']")
- expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/#{path}")
+ context 'when a page does not have history' do
+ before do
+ visit(project_wiki_path(project, wiki_page))
+ end
- click_on('image')
+ it 'shows all the pages' do
+ expect(page).to have_content(user.name)
+ expect(find('.wiki-pages')).to have_content(wiki_page.title.capitalize)
+ end
- expect(current_path).to match("wikis/#{path}")
- expect(page).not_to have_xpath('/html') # Page should render the image which means there is no html involved
- end
- end
+ context 'shows a file stored in a page' do
+ let(:path) { upload_file_to_wiki(project, user, 'dk.png') }
- it 'shows the creation page if file does not exist' do
+ it do
+ expect(page).to have_xpath("//img[@data-src='#{project.wiki.wiki_base_path}/#{path}']")
expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/#{path}")
click_on('image')
expect(current_path).to match("wikis/#{path}")
- expect(page).to have_content('New Wiki Page')
- expect(page).to have_content('Create page')
+ expect(page).not_to have_xpath('/html') # Page should render the image which means there is no html involved
end
end
- context 'when a page has history' do
- before do
- wiki_page.update(message: 'updated home', content: 'updated [some link](other-page)')
- end
+ it 'shows the creation page if file does not exist' do
+ expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/#{path}")
- it 'shows the page history' do
- visit(project_wiki_path(project, wiki_page))
+ click_on('image')
- expect(page).to have_selector('a.btn', text: 'Edit')
+ expect(current_path).to match("wikis/#{path}")
+ expect(page).to have_content('New Wiki Page')
+ expect(page).to have_content('Create page')
+ end
+ end
- click_on('Page history')
+ context 'when a page has history' do
+ before do
+ wiki_page.update(message: 'updated home', content: 'updated [some link](other-page)')
+ end
- expect(page).to have_content(user.name)
- expect(page).to have_content("#{user.username} created page: home")
- expect(page).to have_content('updated home')
- end
+ it 'shows the page history' do
+ visit(project_wiki_path(project, wiki_page))
- it 'does not show the "Edit" button' do
- visit(project_wiki_path(project, wiki_page, version_id: wiki_page.versions.last.id))
+ expect(page).to have_selector('a.btn', text: 'Edit')
- expect(page).not_to have_selector('a.btn', text: 'Edit')
- end
+ click_on('Page history')
+
+ expect(page).to have_content(user.name)
+ expect(page).to have_content("#{user.username} created page: home")
+ expect(page).to have_content('updated home')
end
- context 'when page has invalid content encoding' do
- let(:content) { 'whatever'.force_encoding('ISO-8859-1') }
+ it 'does not show the "Edit" button' do
+ visit(project_wiki_path(project, wiki_page, version_id: wiki_page.versions.last.id))
- before do
- allow(Gitlab::EncodingHelper).to receive(:encode!).and_return(content)
+ expect(page).not_to have_selector('a.btn', text: 'Edit')
+ end
+ end
- visit(project_wiki_path(project, wiki_page))
- end
+ context 'when page has invalid content encoding' do
+ let(:content) { 'whatever'.force_encoding('ISO-8859-1') }
- it 'does not show "Edit" button' do
- expect(page).not_to have_selector('a.btn', text: 'Edit')
- end
+ before do
+ allow(Gitlab::EncodingHelper).to receive(:encode!).and_return(content)
- it 'shows error' do
- page.within(:css, '.flash-notice') do
- expect(page).to have_content('The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository.')
- end
- end
+ visit(project_wiki_path(project, wiki_page))
end
- it 'opens a default wiki page', :js do
- visit(project_path(project))
-
- find('.shortcuts-wiki').click
- click_link "Create your first page"
+ it 'does not show "Edit" button' do
+ expect(page).not_to have_selector('a.btn', text: 'Edit')
+ end
- expect(page).to have_content('Home · Create Page')
+ it 'shows error' do
+ page.within(:css, '.flash-notice') do
+ expect(page).to have_content('The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository.')
+ end
end
end
- context 'when Gitaly is enabled' do
- it_behaves_like 'wiki page user view'
- end
+ it 'opens a default wiki page', :js do
+ visit(project_path(project))
+
+ find('.shortcuts-wiki').click
+ click_link "Create your first page"
- context 'when Gitaly is disabled', :skip_gitaly_mock do
- it_behaves_like 'wiki page user view'
+ expect(page).to have_content('Home · Create Page')
end
end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index fb766addb31..0add129dde2 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -322,6 +322,22 @@ describe 'Project' do
end
end
+ context 'content is not cached after signing out', :js do
+ let(:user) { create(:user, project_view: 'activity') }
+ let(:project) { create(:project, :repository) }
+
+ it 'does not load activity', :js do
+ project.add_maintainer(user)
+ sign_in(user)
+ visit project_path(project)
+ sign_out(user)
+
+ page.evaluate_script('window.history.back()')
+
+ expect(page).not_to have_selector('.event-item')
+ end
+ end
+
def remove_with_confirm(button_text, confirm_with)
click_button button_text
fill_in 'confirm_name_input', with: confirm_with
diff --git a/spec/features/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/search/user_uses_header_search_field_spec.rb b/spec/features/search/user_uses_header_search_field_spec.rb
index af38f77c0c6..444de26733f 100644
--- a/spec/features/search/user_uses_header_search_field_spec.rb
+++ b/spec/features/search/user_uses_header_search_field_spec.rb
@@ -21,13 +21,17 @@ describe 'User uses header search field' do
it 'shows assigned issues' do
find('.search-input-container .dropdown-menu').click_link('Issues assigned to me')
- expect(find('.js-assignee-search')).to have_content(user.name)
+ expect(page).to have_selector('.filtered-search')
+ expect_tokens([assignee_token(user.name)])
+ expect_filtered_search_input_empty
end
it 'shows created issues' do
find('.search-input-container .dropdown-menu').click_link("Issues I've created")
- expect(find('.js-author-search')).to have_content(user.name)
+ expect(page).to have_selector('.filtered-search')
+ expect_tokens([author_token(user.name)])
+ expect_filtered_search_input_empty
end
end
@@ -37,13 +41,17 @@ describe 'User uses header search field' do
it 'shows assigned merge requests' do
find('.search-input-container .dropdown-menu').click_link('Merge requests assigned to me')
- expect(find('.js-assignee-search')).to have_content(user.name)
+ expect(page).to have_selector('.filtered-search')
+ expect_tokens([assignee_token(user.name)])
+ expect_filtered_search_input_empty
end
it 'shows created merge requests' do
find('.search-input-container .dropdown-menu').click_link("Merge requests I've created")
- expect(find('.js-author-search')).to have_content(user.name)
+ expect(page).to have_selector('.filtered-search')
+ expect_tokens([author_token(user.name)])
+ expect_filtered_search_input_empty
end
end
end
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/features/users/login_spec.rb b/spec/features/users/login_spec.rb
index 44758f862a8..ad856bd062e 100644
--- a/spec/features/users/login_spec.rb
+++ b/spec/features/users/login_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe 'Login' do
include TermsHelper
+ include UserLoginHelper
before do
stub_authentication_activity_metrics(debug: true)
@@ -546,29 +547,6 @@ describe 'Login' do
ensure_tab_pane_correctness(false)
end
end
-
- def ensure_tab_pane_correctness(visit_path = true)
- if visit_path
- visit new_user_session_path
- end
-
- ensure_tab_pane_counts
- ensure_one_active_tab
- ensure_one_active_pane
- end
-
- def ensure_tab_pane_counts
- tabs_count = page.all('[role="tab"]').size
- expect(page).to have_selector('[role="tabpanel"]', count: tabs_count)
- end
-
- def ensure_one_active_tab
- expect(page).to have_selector('ul.new-session-tabs > li > a.active', count: 1)
- end
-
- def ensure_one_active_pane
- expect(page).to have_selector('.tab-pane.active', count: 1)
- end
end
context 'when terms are enforced' do
diff --git a/spec/features/users/overview_spec.rb b/spec/features/users/overview_spec.rb
index b0ff53f9ccb..34ed771340f 100644
--- a/spec/features/users/overview_spec.rb
+++ b/spec/features/users/overview_spec.rb
@@ -54,15 +54,15 @@ describe 'Overview tab on a user profile', :js do
end
end
- describe 'user has 10 activities' do
+ describe 'user has 11 activities' do
before do
- 10.times { push_code_contribution }
+ 11.times { push_code_contribution }
end
include_context 'visit overview tab'
- it 'displays 5 entries in the list of activities' do
- expect(find('#js-overview')).to have_selector('.event-item', count: 5)
+ it 'displays 10 entries in the list of activities' do
+ expect(find('#js-overview')).to have_selector('.event-item', count: 10)
end
it 'shows a link to the activity list' do
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/group_descendants_finder_spec.rb b/spec/finders/group_descendants_finder_spec.rb
index c64abdc3619..c28fd7cad11 100644
--- a/spec/finders/group_descendants_finder_spec.rb
+++ b/spec/finders/group_descendants_finder_spec.rb
@@ -74,6 +74,13 @@ describe GroupDescendantsFinder do
end
end
+ it 'sorts elements by latest created as default' do
+ project1 = create(:project, namespace: group, created_at: 1.hour.ago)
+ project2 = create(:project, namespace: group)
+
+ expect(subject.execute).to eq([project2, project1])
+ end
+
context 'sorting by name' do
let!(:project1) { create(:project, namespace: group, name: 'a', path: 'project-a') }
let!(:project2) { create(:project, namespace: group, name: 'z', path: 'project-z') }
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 0689c843104..c0488c83bd8 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -57,13 +57,39 @@ describe IssuesFinder do
end
context 'filtering by no assignee' do
- let(:params) { { assignee_id: 0 } }
+ let(:params) { { assignee_id: 'None' } }
+
+ it 'returns issues not assigned to any assignee' do
+ expect(issues).to contain_exactly(issue4)
+ end
+
+ it 'returns issues not assigned to any assignee' do
+ params[:assignee_id] = 0
+
+ expect(issues).to contain_exactly(issue4)
+ end
+
+ it 'returns issues not assigned to any assignee' do
+ params[:assignee_id] = 'none'
- it 'returns issues not assign to any assignee' do
expect(issues).to contain_exactly(issue4)
end
end
+ context 'filtering by any assignee' do
+ let(:params) { { assignee_id: 'Any' } }
+
+ it 'returns issues assigned to any assignee' do
+ expect(issues).to contain_exactly(issue1, issue2, issue3)
+ end
+
+ it 'returns issues assigned to any assignee' do
+ params[:assignee_id] = 'any'
+
+ expect(issues).to contain_exactly(issue1, issue2, issue3)
+ end
+ end
+
context 'filtering by group_id' do
let(:params) { { group_id: group.id } }
@@ -118,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
@@ -334,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/pending_todos_finder_spec.rb b/spec/finders/pending_todos_finder_spec.rb
index 32fad5e225f..b41b1b46a93 100644
--- a/spec/finders/pending_todos_finder_spec.rb
+++ b/spec/finders/pending_todos_finder_spec.rb
@@ -46,7 +46,7 @@ describe PendingTodosFinder do
create(:todo, :pending, user: user, target: note)
- todos = described_class.new(user, target_type: issue.class).execute
+ todos = described_class.new(user, target_type: issue.class.name).execute
expect(todos).to eq([todo])
end
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/pipelines_finder_spec.rb b/spec/finders/pipelines_finder_spec.rb
index c6e832ad69b..c2c304589c9 100644
--- a/spec/finders/pipelines_finder_spec.rb
+++ b/spec/finders/pipelines_finder_spec.rb
@@ -225,7 +225,7 @@ describe PipelinesFinder do
end
end
- context 'when the project has limited access to piplines' do
+ context 'when the project has limited access to pipelines' do
let(:project) { create(:project, :private, :repository) }
let(:current_user) { create(:user) }
let!(:pipelines) { create_list(:ci_pipeline, 2, project: project) }
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/entities/merge_request_widget.json b/spec/fixtures/api/schemas/entities/merge_request_widget.json
index c40977bc4ee..35971d564d5 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_widget.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_widget.json
@@ -46,6 +46,7 @@
"diff_head_commit_short_id": { "type": ["string", "null"] },
"merge_commit_message": { "type": ["string", "null"] },
"pipeline": { "type": ["object", "null"] },
+ "merge_pipeline": { "type": ["object", "null"] },
"work_in_progress": { "type": "boolean" },
"source_branch_exists": { "type": "boolean" },
"mergeable_discussions_state": { "type": "boolean" },
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/job/job_details.json b/spec/fixtures/api/schemas/job/job_details.json
index 8218474705c..cdf7b049ab6 100644
--- a/spec/fixtures/api/schemas/job/job_details.json
+++ b/spec/fixtures/api/schemas/job/job_details.json
@@ -18,6 +18,7 @@
"runner": { "$ref": "runner.json" },
"runners": { "$ref": "runners.json" },
"has_trace": { "type": "boolean" },
- "stage": { "type": "string" }
+ "stage": { "type": "string" },
+ "stuck": { "type": "boolean" }
}
}
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/fixtures/trace/sample_trace b/spec/fixtures/trace/sample_trace
index 7bfe3f83b7b..3d8beb0dec2 100644
--- a/spec/fixtures/trace/sample_trace
+++ b/spec/fixtures/trace/sample_trace
@@ -2334,12 +2334,12 @@ Boards::Lists::MoveService
keeps position of lists when list type is closed
when list type is set to label
keeps position of lists when new position is nil
- keeps position of lists when new positon is equal to old position
- keeps position of lists when new positon is negative
- keeps position of lists when new positon is equal to number of labels lists
- keeps position of lists when new positon is greater than number of labels lists
- increments position of intermediate lists when new positon is equal to first position
- decrements position of intermediate lists when new positon is equal to last position
+ keeps position of lists when new position is equal to old position
+ keeps position of lists when new position is negative
+ keeps position of lists when new position is equal to number of labels lists
+ keeps position of lists when new position is greater than number of labels lists
+ increments position of intermediate lists when new position is equal to first position
+ decrements position of intermediate lists when new position is equal to last position
decrements position of intermediate lists when new position is greater than old position
increments position of intermediate lists when new position is lower than old position
when board parent is a group
@@ -2347,12 +2347,12 @@ Boards::Lists::MoveService
keeps position of lists when list type is closed
when list type is set to label
keeps position of lists when new position is nil
- keeps position of lists when new positon is equal to old position
- keeps position of lists when new positon is negative
- keeps position of lists when new positon is equal to number of labels lists
- keeps position of lists when new positon is greater than number of labels lists
- increments position of intermediate lists when new positon is equal to first position
- decrements position of intermediate lists when new positon is equal to last position
+ keeps position of lists when new position is equal to old position
+ keeps position of lists when new position is negative
+ keeps position of lists when new position is equal to number of labels lists
+ keeps position of lists when new position is greater than number of labels lists
+ increments position of intermediate lists when new position is equal to first position
+ decrements position of intermediate lists when new position is equal to last position
decrements position of intermediate lists when new position is greater than old position
increments position of intermediate lists when new position is lower than old position
diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb
index 120b23e66ac..f0c2e4768ec 100644
--- a/spec/helpers/auth_helper_spec.rb
+++ b/spec/helpers/auth_helper_spec.rb
@@ -42,6 +42,16 @@ describe AuthHelper do
end
end
+ describe 'form_based_auth_provider_has_active_class?' do
+ it 'selects main LDAP server' do
+ allow(helper).to receive(:auth_providers) { [:twitter, :ldapprimary, :ldapsecondary, :kerberos] }
+ expect(helper.form_based_auth_provider_has_active_class?(:twitter)).to be(false)
+ expect(helper.form_based_auth_provider_has_active_class?(:ldapprimary)).to be(true)
+ expect(helper.form_based_auth_provider_has_active_class?(:ldapsecondary)).to be(false)
+ expect(helper.form_based_auth_provider_has_active_class?(:kerberos)).to be(false)
+ end
+ end
+
describe 'enabled_button_based_providers' do
before do
allow(helper).to receive(:auth_providers) { [:twitter, :github] }
diff --git a/spec/helpers/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/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index 8bfd520528f..4945749f524 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -135,5 +135,40 @@ describe SearchHelper do
expect(search_filter_input_options('')[:data]['base-endpoint']).to eq("/groups#{group_path(@group)}")
end
end
+
+ context 'dashboard' do
+ it 'does not include group-id and project-id' do
+ expect(search_filter_input_options('')[:data]['project-id']).to eq(nil)
+ expect(search_filter_input_options('')[:data]['group-id']).to eq(nil)
+ end
+
+ it 'includes dashboard base-endpoint' do
+ expect(search_filter_input_options('')[:data]['base-endpoint']).to eq("/dashboard")
+ end
+ end
+ end
+
+ describe 'search_history_storage_prefix' do
+ context 'project' do
+ it 'returns project full_path' do
+ @project = create(:project, :repository)
+
+ expect(search_history_storage_prefix).to eq(@project.full_path)
+ end
+ end
+
+ context 'group' do
+ it 'returns group full_path' do
+ @group = create(:group, :nested, name: 'group-name')
+
+ expect(search_history_storage_prefix).to eq(@group.full_path)
+ end
+ end
+
+ context 'dashboard' do
+ it 'returns dashboard' do
+ expect(search_history_storage_prefix).to eq("dashboard")
+ end
+ end
end
end
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/ci_variable_list/ajax_variable_list_spec.js b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js
index 4f8701bae01..1fc0e206d5e 100644
--- a/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js
+++ b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js
@@ -24,7 +24,7 @@ describe('AjaxFormVariableList', () => {
mock = new MockAdapter(axios);
const ajaxVariableListEl = document.querySelector('.js-ci-variable-list-section');
- saveButton = ajaxVariableListEl.querySelector('.js-secret-variables-save-button');
+ saveButton = ajaxVariableListEl.querySelector('.js-ci-variables-save-button');
errorBox = container.querySelector('.js-ci-variable-error-box');
ajaxVariableList = new AjaxFormVariableList({
container,
@@ -44,7 +44,7 @@ describe('AjaxFormVariableList', () => {
describe('onSaveClicked', () => {
it('shows loading spinner while waiting for the request', done => {
- const loadingIcon = saveButton.querySelector('.js-secret-variables-save-loading-icon');
+ const loadingIcon = saveButton.querySelector('.js-ci-variables-save-loading-icon');
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(() => {
expect(loadingIcon.classList.contains(HIDE_CLASS)).toEqual(false);
@@ -172,7 +172,7 @@ describe('AjaxFormVariableList', () => {
container = document.querySelector('.js-ci-variable-list-section');
const ajaxVariableListEl = document.querySelector('.js-ci-variable-list-section');
- saveButton = ajaxVariableListEl.querySelector('.js-secret-variables-save-button');
+ saveButton = ajaxVariableListEl.querySelector('.js-ci-variables-save-button');
errorBox = container.querySelector('.js-ci-variable-error-box');
ajaxVariableList = new AjaxFormVariableList({
container,
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/compare_versions_spec.js b/spec/javascripts/diffs/components/compare_versions_spec.js
index 7237274eb43..d9d7f61785f 100644
--- a/spec/javascripts/diffs/components/compare_versions_spec.js
+++ b/spec/javascripts/diffs/components/compare_versions_spec.js
@@ -1 +1,125 @@
-// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+import Vue from 'vue';
+import CompareVersionsComponent from '~/diffs/components/compare_versions.vue';
+import store from '~/mr_notes/stores';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import diffsMockData from '../mock_data/merge_request_diffs';
+
+describe('CompareVersions', () => {
+ let vm;
+ const targetBranch = { branchName: 'tmp-wine-dev', versionIndex: -1 };
+
+ beforeEach(() => {
+ vm = createComponentWithStore(Vue.extend(CompareVersionsComponent), store, {
+ mergeRequestDiffs: diffsMockData,
+ mergeRequestDiff: diffsMockData[0],
+ targetBranch,
+ }).$mount();
+ });
+
+ describe('template', () => {
+ it('should render Tree List toggle button with correct attribute values', () => {
+ const treeListBtn = vm.$el.querySelector('.js-toggle-tree-list');
+
+ expect(treeListBtn).not.toBeNull();
+ expect(treeListBtn.dataset.originalTitle).toBe('Toggle file browser');
+ expect(treeListBtn.querySelectorAll('svg use').length).not.toBe(0);
+ expect(treeListBtn.querySelector('svg use').getAttribute('xlink:href')).toContain(
+ '#hamburger',
+ );
+ });
+
+ it('should render comparison dropdowns with correct values', () => {
+ const sourceDropdown = vm.$el.querySelector('.mr-version-dropdown');
+ const targetDropdown = vm.$el.querySelector('.mr-version-compare-dropdown');
+
+ expect(sourceDropdown).not.toBeNull();
+ expect(targetDropdown).not.toBeNull();
+ expect(sourceDropdown.querySelector('a span').innerHTML).toContain('latest version');
+ expect(targetDropdown.querySelector('a span').innerHTML).toContain(targetBranch.branchName);
+ });
+
+ it('should not render comparison dropdowns if no mergeRequestDiffs are specified', () => {
+ vm.mergeRequestDiffs = [];
+
+ vm.$nextTick(() => {
+ const sourceDropdown = vm.$el.querySelector('.mr-version-dropdown');
+ const targetDropdown = vm.$el.querySelector('.mr-version-compare-dropdown');
+
+ expect(sourceDropdown).toBeNull();
+ expect(targetDropdown).toBeNull();
+ });
+ });
+
+ it('should render whitespace toggle button with correct attributes', () => {
+ const whitespaceBtn = vm.$el.querySelector('.qa-toggle-whitespace');
+ const href = vm.toggleWhitespacePath;
+
+ expect(whitespaceBtn).not.toBeNull();
+ expect(whitespaceBtn.getAttribute('href')).toEqual(href);
+ expect(whitespaceBtn.innerHTML).toContain('Hide whitespace changes');
+ });
+
+ it('should render view types buttons with correct values', () => {
+ const inlineBtn = vm.$el.querySelector('#inline-diff-btn');
+ const parallelBtn = vm.$el.querySelector('#parallel-diff-btn');
+
+ expect(inlineBtn).not.toBeNull();
+ expect(parallelBtn).not.toBeNull();
+ expect(inlineBtn.dataset.viewType).toEqual('inline');
+ expect(parallelBtn.dataset.viewType).toEqual('parallel');
+ expect(inlineBtn.innerHTML).toContain('Inline');
+ expect(parallelBtn.innerHTML).toContain('Side-by-side');
+ });
+ });
+
+ describe('setInlineDiffViewType', () => {
+ it('should persist the view type in the url', () => {
+ const viewTypeBtn = vm.$el.querySelector('#inline-diff-btn');
+ viewTypeBtn.click();
+
+ expect(window.location.toString()).toContain('?view=inline');
+ });
+ });
+
+ describe('setParallelDiffViewType', () => {
+ it('should persist the view type in the url', () => {
+ const viewTypeBtn = vm.$el.querySelector('#parallel-diff-btn');
+ viewTypeBtn.click();
+
+ expect(window.location.toString()).toContain('?view=parallel');
+ });
+ });
+
+ describe('comparableDiffs', () => {
+ it('should not contain the first item in the mergeRequestDiffs property', () => {
+ const { comparableDiffs } = vm;
+ const comparableDiffsMock = diffsMockData.slice(1);
+
+ expect(comparableDiffs).toEqual(comparableDiffsMock);
+ });
+ });
+
+ describe('isWhitespaceVisible', () => {
+ const originalHref = window.location.href;
+
+ afterEach(() => {
+ window.history.replaceState({}, null, originalHref);
+ });
+
+ it('should return "true" when no "w" flag is present in the URL (default)', () => {
+ expect(vm.isWhitespaceVisible()).toBe(true);
+ });
+
+ it('should return "false" when the flag is set to "1" in the URL', () => {
+ window.history.replaceState({}, null, '?w=1');
+
+ expect(vm.isWhitespaceVisible()).toBe(false);
+ });
+
+ it('should return "true" when the flag is set to "0" in the URL', () => {
+ window.history.replaceState({}, null, '?w=0');
+
+ expect(vm.isWhitespaceVisible()).toBe(true);
+ });
+ });
+});
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/mock_data/merge_request_diffs.js b/spec/javascripts/diffs/mock_data/merge_request_diffs.js
new file mode 100644
index 00000000000..d72ad7818dd
--- /dev/null
+++ b/spec/javascripts/diffs/mock_data/merge_request_diffs.js
@@ -0,0 +1,42 @@
+export default [
+ {
+ versionIndex: 4,
+ createdAt: '2018-10-23T11:49:16.611Z',
+ commitsCount: 4,
+ latest: true,
+ shortCommitSha: 'de7a8f7f',
+ versionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37',
+ comparePath:
+ '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=de7a8f7f20c3ea2e0bef3ba01cfd41c21f6b4995',
+ },
+ {
+ versionIndex: 3,
+ createdAt: '2018-10-23T11:46:40.617Z',
+ commitsCount: 3,
+ latest: false,
+ shortCommitSha: 'e78fc18f',
+ versionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=36',
+ comparePath:
+ '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=e78fc18fa37acb2185c59ca94d4a964464feb50e',
+ },
+ {
+ versionIndex: 2,
+ createdAt: '2018-10-04T09:57:39.648Z',
+ commitsCount: 2,
+ latest: false,
+ shortCommitSha: '48da7e7e',
+ versionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=35',
+ comparePath:
+ '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=48da7e7e9a99d41c852578bd9cb541ca4d864b3e',
+ },
+ {
+ versionIndex: 1,
+ createdAt: '2018-09-25T20:30:39.493Z',
+ commitsCount: 1,
+ latest: false,
+ shortCommitSha: '47bac2ed',
+ versionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=20',
+ comparePath:
+ '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=47bac2ed972c5bee344c1cea159a22cd7f711dc0',
+ },
+];
diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js
index 85c1926fcb1..acd95a3dd8b 100644
--- a/spec/javascripts/diffs/store/actions_spec.js
+++ b/spec/javascripts/diffs/store/actions_spec.js
@@ -27,7 +27,6 @@ import actions, {
toggleShowTreeList,
} from '~/diffs/store/actions';
import * as types from '~/diffs/store/mutation_types';
-import { reduceDiscussionsToLineCodes } from '~/notes/stores/utils';
import axios from '~/lib/utils/axios_utils';
import testAction from '../../helpers/vuex_action_helper';
@@ -98,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 = {
@@ -146,13 +145,13 @@ describe('DiffsStoreActions', () => {
diff_file: {
file_hash: 'ABC',
},
- fileHash: 'ABC',
+ file_hash: 'ABC',
resolvable: true,
position: diffPosition,
original_position: diffPosition,
};
- const discussions = reduceDiscussionsToLineCodes([singleDiscussion]);
+ const discussions = [singleDiscussion];
testAction(
assignDiscussionsToDiff,
@@ -162,28 +161,25 @@ describe('DiffsStoreActions', () => {
{
type: types.SET_LINE_DISCUSSIONS_FOR_FILE,
payload: {
- fileHash: 'ABC',
- discussions: [singleDiscussion],
+ 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,
);
});
});
@@ -193,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,
@@ -205,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: [],
},
],
@@ -220,7 +216,8 @@ describe('DiffsStoreActions', () => {
],
};
const singleDiscussion = {
- fileHash: 'ABC',
+ id: '1',
+ file_hash: 'ABC',
line_code: 'ABC_1_1',
};
@@ -232,15 +229,14 @@ describe('DiffsStoreActions', () => {
{
type: types.REMOVE_LINE_DISCUSSIONS_FOR_FILE,
payload: {
+ id: '1',
fileHash: 'ABC',
lineCode: 'ABC_1_1',
},
},
],
[],
- () => {
- done();
- },
+ done,
);
});
});
@@ -420,7 +416,7 @@ describe('DiffsStoreActions', () => {
const getters = {
getDiffFileDiscussions: jasmine.createSpy().and.returnValue([{ id: 1 }]),
diffHasAllExpandedDiscussions: jasmine.createSpy().and.returnValue(true),
- diffHasAllCollpasedDiscussions: jasmine.createSpy().and.returnValue(false),
+ diffHasAllCollapsedDiscussions: jasmine.createSpy().and.returnValue(false),
};
const dispatch = jasmine.createSpy('dispatch');
@@ -438,7 +434,7 @@ describe('DiffsStoreActions', () => {
const getters = {
getDiffFileDiscussions: jasmine.createSpy().and.returnValue([{ id: 1 }]),
diffHasAllExpandedDiscussions: jasmine.createSpy().and.returnValue(false),
- diffHasAllCollpasedDiscussions: jasmine.createSpy().and.returnValue(true),
+ diffHasAllCollapsedDiscussions: jasmine.createSpy().and.returnValue(true),
};
const dispatch = jasmine.createSpy();
@@ -456,7 +452,7 @@ describe('DiffsStoreActions', () => {
const getters = {
getDiffFileDiscussions: jasmine.createSpy().and.returnValue([{ expanded: false, id: 1 }]),
diffHasAllExpandedDiscussions: jasmine.createSpy().and.returnValue(false),
- diffHasAllCollpasedDiscussions: jasmine.createSpy().and.returnValue(false),
+ diffHasAllCollapsedDiscussions: jasmine.createSpy().and.returnValue(false),
};
const dispatch = jasmine.createSpy();
@@ -581,7 +577,6 @@ describe('DiffsStoreActions', () => {
describe('saveDiffDiscussion', () => {
beforeEach(() => {
spyOnDependency(actions, 'getNoteFormData').and.returnValue('testData');
- spyOnDependency(actions, 'reduceDiscussionsToLineCodes').and.returnValue('discussions');
});
it('dispatches actions', done => {
@@ -602,7 +597,7 @@ describe('DiffsStoreActions', () => {
.then(() => {
expect(dispatch.calls.argsFor(0)).toEqual(['saveNote', 'testData', { root: true }]);
expect(dispatch.calls.argsFor(1)).toEqual(['updateDiscussion', 'test', { root: true }]);
- expect(dispatch.calls.argsFor(2)).toEqual(['assignDiscussionsToDiff', 'discussions']);
+ expect(dispatch.calls.argsFor(2)).toEqual(['assignDiscussionsToDiff', ['discussion']]);
})
.then(done)
.catch(done.fail);
diff --git a/spec/javascripts/diffs/store/getters_spec.js b/spec/javascripts/diffs/store/getters_spec.js
index 807a9e3baf0..eef95c823fb 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);
});
});
@@ -106,13 +106,13 @@ describe('Diffs Module Getters', () => {
});
});
- describe('diffHasAllCollpasedDiscussions', () => {
+ describe('diffHasAllCollapsedDiscussions', () => {
it('returns true when all discussions are collapsed', () => {
discussionMock.diff_file.file_hash = diffFileMock.fileHash;
discussionMock.expanded = false;
expect(
- getters.diffHasAllCollpasedDiscussions(localState, {
+ getters.diffHasAllCollapsedDiscussions(localState, {
getDiffFileDiscussions: () => [discussionMock],
})(diffFileMock),
).toEqual(true);
@@ -120,7 +120,7 @@ describe('Diffs Module Getters', () => {
it('returns false when there are no discussions', () => {
expect(
- getters.diffHasAllCollpasedDiscussions(localState, {
+ getters.diffHasAllCollapsedDiscussions(localState, {
getDiffFileDiscussions: () => [],
})(diffFileMock),
).toEqual(false);
@@ -130,7 +130,7 @@ describe('Diffs Module Getters', () => {
discussionMock1.expanded = false;
expect(
- getters.diffHasAllCollpasedDiscussions(localState, {
+ getters.diffHasAllCollapsedDiscussions(localState, {
getDiffFileDiscussions: () => [discussionMock, discussionMock1],
})(diffFileMock),
).toEqual(false);
@@ -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 b7e28391419..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,164 +144,147 @@ 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: [],
},
],
},
],
};
- const discussions = [
- {
- id: 1,
- line_code: 'ABC_1',
- diff_discussion: true,
- resolvable: true,
- original_position: diffPosition,
- position: diffPosition,
- },
- {
- id: 2,
- line_code: 'ABC_1',
- diff_discussion: true,
- resolvable: true,
- original_position: diffPosition,
- position: diffPosition,
+ const discussion = {
+ id: 1,
+ line_code: 'ABC_1',
+ diff_discussion: true,
+ resolvable: true,
+ original_position: diffPosition,
+ position: diffPosition,
+ diff_file: {
+ file_hash: state.diffFiles[0].file_hash,
},
- ];
+ };
const diffPositionByLineCode = {
ABC_1: diffPosition,
};
mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, {
- fileHash: 'ABC',
- discussions,
+ discussion,
diffPositionByLineCode,
});
- expect(state.diffFiles[0].parallelDiffLines[0].left.discussions.length).toEqual(2);
- expect(state.diffFiles[0].parallelDiffLines[0].left.discussions[1].id).toEqual(2);
+ 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(2);
- expect(state.diffFiles[0].highlightedDiffLines[0].discussions[1].id).toEqual(2);
+ 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: [],
},
],
},
],
};
- const discussions = [
- {
- id: 1,
- line_code: 'ABC_1',
- diff_discussion: true,
- active: true,
- },
- {
- id: 2,
- line_code: 'ABC_1',
- diff_discussion: true,
- active: true,
+ const discussion = {
+ id: 1,
+ line_code: 'ABC_1',
+ diff_discussion: true,
+ active: true,
+ diff_file: {
+ file_hash: state.diffFiles[0].file_hash,
},
- ];
+ };
const diffPositionByLineCode = {
ABC_1: diffPosition,
};
mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, {
- fileHash: 'ABC',
- discussions,
+ discussion,
diffPositionByLineCode,
});
- expect(state.diffFiles[0].parallelDiffLines[0].left.discussions.length).toEqual(2);
- expect(state.diffFiles[0].parallelDiffLines[0].left.discussions[1].id).toEqual(2);
+ 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(2);
- expect(state.diffFiles[0].highlightedDiffLines[0].discussions[1].id).toEqual(2);
+ 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);
});
});
@@ -310,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,
@@ -327,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,
@@ -356,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/ide/stores/modules/pipelines/actions_spec.js b/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js
index d85354c3681..c9c09ee9afe 100644
--- a/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js
+++ b/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js
@@ -77,7 +77,7 @@ describe('IDE pipelines actions', () => {
{
type: 'setErrorMessage',
payload: {
- text: 'An error occured whilst fetching the latest pipline.',
+ text: 'An error occured whilst fetching the latest pipeline.',
action: jasmine.any(Function),
actionText: 'Please try again',
actionPayload: null,
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/job_spec.js b/spec/javascripts/job_spec.js
deleted file mode 100644
index bc973407b25..00000000000
--- a/spec/javascripts/job_spec.js
+++ /dev/null
@@ -1,265 +0,0 @@
-// import $ from 'jquery';
-// import MockAdapter from 'axios-mock-adapter';
-// import axios from '~/lib/utils/axios_utils';
-// import { numberToHumanSize } from '~/lib/utils/number_utils';
-// import '~/lib/utils/datetime_utility';
-// import Job from '~/job';
-// import '~/breakpoints';
-// import waitForPromises from 'spec/helpers/wait_for_promises';
-
-// describe('Job', () => {
-// const JOB_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`;
-// let mock;
-// let response;
-// let job;
-
-// preloadFixtures('builds/build-with-artifacts.html.raw');
-
-// beforeEach(() => {
-// loadFixtures('builds/build-with-artifacts.html.raw');
-
-// spyOnDependency(Job, 'visitUrl');
-
-// response = {};
-
-// mock = new MockAdapter(axios);
-
-// mock.onGet(new RegExp(`${JOB_URL}/trace.json?(.*)`)).reply(() => [200, response]);
-// });
-
-// afterEach(() => {
-// mock.restore();
-
-// clearTimeout(job.timeout);
-// });
-
-// describe('class constructor', () => {
-// beforeEach(() => {
-// jasmine.clock().install();
-// });
-
-// afterEach(() => {
-// jasmine.clock().uninstall();
-// });
-
-// describe('running build', () => {
-// it('updates the build trace on an interval', function (done) {
-// response = {
-// html: '<span>Update<span>',
-// status: 'running',
-// state: 'newstate',
-// append: true,
-// complete: false,
-// };
-
-// job = new Job();
-
-// waitForPromises()
-// .then(() => {
-// expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
-// expect(job.state).toBe('newstate');
-
-// response = {
-// html: '<span>More</span>',
-// status: 'running',
-// state: 'finalstate',
-// append: true,
-// complete: true,
-// };
-// })
-// .then(() => jasmine.clock().tick(4001))
-// .then(waitForPromises)
-// .then(() => {
-// expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/);
-// expect(job.state).toBe('finalstate');
-// })
-// .then(done)
-// .catch(done.fail);
-// });
-
-// it('replaces the entire build trace', (done) => {
-// response = {
-// html: '<span>Update<span>',
-// status: 'running',
-// append: false,
-// complete: false,
-// };
-
-// job = new Job();
-
-// waitForPromises()
-// .then(() => {
-// expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
-
-// response = {
-// html: '<span>Different</span>',
-// status: 'running',
-// append: false,
-// };
-// })
-// .then(() => jasmine.clock().tick(4001))
-// .then(waitForPromises)
-// .then(() => {
-// expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/);
-// expect($('#build-trace .js-build-output').text()).toMatch(/Different/);
-// })
-// .then(done)
-// .catch(done.fail);
-// });
-// });
-
-// describe('truncated information', () => {
-// describe('when size is less than total', () => {
-// it('shows information about truncated log', (done) => {
-// response = {
-// html: '<span>Update</span>',
-// status: 'success',
-// append: false,
-// size: 50,
-// total: 100,
-// };
-
-// job = new Job();
-
-// waitForPromises()
-// .then(() => {
-// expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden');
-// })
-// .then(done)
-// .catch(done.fail);
-// });
-
-// it('shows the size in KiB', (done) => {
-// const size = 50;
-
-// response = {
-// html: '<span>Update</span>',
-// status: 'success',
-// append: false,
-// size,
-// total: 100,
-// };
-
-// job = new Job();
-
-// waitForPromises()
-// .then(() => {
-// expect(
-// document.querySelector('.js-truncated-info-size').textContent.trim(),
-// ).toEqual(`${numberToHumanSize(size)}`);
-// })
-// .then(done)
-// .catch(done.fail);
-// });
-
-// it('shows incremented size', (done) => {
-// response = {
-// html: '<span>Update</span>',
-// status: 'success',
-// append: false,
-// size: 50,
-// total: 100,
-// complete: false,
-// };
-
-// job = new Job();
-
-// waitForPromises()
-// .then(() => {
-// expect(
-// document.querySelector('.js-truncated-info-size').textContent.trim(),
-// ).toEqual(`${numberToHumanSize(50)}`);
-
-// response = {
-// html: '<span>Update</span>',
-// status: 'success',
-// append: true,
-// size: 10,
-// total: 100,
-// complete: true,
-// };
-// })
-// .then(() => jasmine.clock().tick(4001))
-// .then(waitForPromises)
-// .then(() => {
-// expect(
-// document.querySelector('.js-truncated-info-size').textContent.trim(),
-// ).toEqual(`${numberToHumanSize(60)}`);
-// })
-// .then(done)
-// .catch(done.fail);
-// });
-
-// it('renders the raw link', () => {
-// response = {
-// html: '<span>Update</span>',
-// status: 'success',
-// append: false,
-// size: 50,
-// total: 100,
-// };
-
-// job = new Job();
-
-// expect(
-// document.querySelector('.js-raw-link').textContent.trim(),
-// ).toContain('Complete Raw');
-// });
-// });
-
-// describe('when size is equal than total', () => {
-// it('does not show the trunctated information', (done) => {
-// response = {
-// html: '<span>Update</span>',
-// status: 'success',
-// append: false,
-// size: 100,
-// total: 100,
-// };
-
-// job = new Job();
-
-// waitForPromises()
-// .then(() => {
-// expect(document.querySelector('.js-truncated-info').classList).toContain('hidden');
-// })
-// .then(done)
-// .catch(done.fail);
-// });
-// });
-// });
-
-// describe('output trace', () => {
-// beforeEach((done) => {
-// response = {
-// html: '<span>Update</span>',
-// status: 'success',
-// append: false,
-// size: 50,
-// total: 100,
-// };
-
-// job = new Job();
-
-// waitForPromises()
-// .then(done)
-// .catch(done.fail);
-// });
-
-// it('should render trace controls', () => {
-// const controllers = document.querySelector('.controllers');
-
-// expect(controllers.querySelector('.js-raw-link-controller')).not.toBeNull();
-// expect(controllers.querySelector('.js-scroll-up')).not.toBeNull();
-// expect(controllers.querySelector('.js-scroll-down')).not.toBeNull();
-// });
-
-// it('should render received output', () => {
-// expect(
-// document.querySelector('.js-build-output').innerHTML,
-// ).toEqual('<span>Update</span>');
-// });
-// });
-// });
-
-// });
diff --git a/spec/javascripts/jobs/components/empty_state_spec.js b/spec/javascripts/jobs/components/empty_state_spec.js
index 0a39709221c..a2df79bdda0 100644
--- a/spec/javascripts/jobs/components/empty_state_spec.js
+++ b/spec/javascripts/jobs/components/empty_state_spec.js
@@ -84,6 +84,7 @@ describe('Empty State', () => {
vm = mountComponent(Component, {
...props,
content,
+ action: null,
});
expect(vm.$el.querySelector('.js-job-empty-state-action')).toBeNull();
diff --git a/spec/javascripts/jobs/components/job_app_spec.js b/spec/javascripts/jobs/components/job_app_spec.js
index e6d403dc826..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, {});
});
@@ -88,7 +89,9 @@ describe('Job App ', () => {
describe('triggered job', () => {
beforeEach(() => {
- mock.onGet(props.endpoint).replyOnce(200, Object.assign({}, job, { started: '2017-05-24T10:59:52.000+01:00' }));
+ mock
+ .onGet(props.endpoint)
+ .replyOnce(200, Object.assign({}, job, { started: '2017-05-24T10:59:52.000+01:00' }));
vm = mountComponentWithStore(Component, { props, store });
});
@@ -99,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);
});
@@ -125,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);
});
@@ -133,57 +136,106 @@ describe('Job App ', () => {
});
describe('stuck block', () => {
- it('renders stuck block when there are no runners', done => {
- mock.onGet(props.endpoint).replyOnce(
- 200,
- Object.assign({}, job, {
- status: {
- group: 'pending',
- icon: 'status_pending',
- label: 'pending',
- text: 'pending',
- details_path: 'path',
- },
- runners: {
- available: false,
- },
- }),
- );
- vm = mountComponentWithStore(Component, { props, store });
-
- setTimeout(() => {
- expect(vm.$el.querySelector('.js-job-stuck')).not.toBeNull();
+ describe('without active runners availabl', () => {
+ it('renders stuck block when there are no runners', done => {
+ mock.onGet(props.endpoint).replyOnce(
+ 200,
+ Object.assign({}, job, {
+ status: {
+ group: 'pending',
+ icon: 'status_pending',
+ label: 'pending',
+ text: 'pending',
+ details_path: 'path',
+ },
+ stuck: true,
+ runners: {
+ available: false,
+ online: false,
+ },
+ tags: [],
+ }),
+ );
+ vm = mountComponentWithStore(Component, { props, store });
- done();
- }, 0);
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.js-job-stuck')).not.toBeNull();
+ expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(
+ "This job is stuck, because you don't have any active runners that can run this job.",
+ );
+ done();
+ }, 0);
+ });
});
- it('renders tags in stuck block when there are no runners', done => {
- mock.onGet(props.endpoint).replyOnce(
- 200,
- Object.assign({}, job, {
- status: {
- group: 'pending',
- icon: 'status_pending',
- label: 'pending',
- text: 'pending',
- details_path: 'path',
- },
- runners: {
- available: false,
- },
- }),
- );
+ describe('when available runners can not run specified tag', () => {
+ it('renders tags in stuck block when there are no runners', done => {
+ mock.onGet(props.endpoint).replyOnce(
+ 200,
+ Object.assign({}, job, {
+ status: {
+ group: 'pending',
+ icon: 'status_pending',
+ label: 'pending',
+ text: 'pending',
+ details_path: 'path',
+ },
+ stuck: true,
+ runners: {
+ available: false,
+ online: false,
+ },
+ }),
+ );
- vm = mountComponentWithStore(Component, {
- props,
- store,
+ vm = mountComponentWithStore(Component, {
+ props,
+ store,
+ });
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]);
+ expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(
+ "This job is stuck, because you don't have any active runners online with any of these tags assigned to them:",
+ );
+ done();
+ }, 0);
});
+ });
- setTimeout(() => {
- expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]);
- done();
- }, 0);
+ describe('when runners are offline and build has tags', () => {
+ it('renders message about job being stuck because of no runners with the specified tags', done => {
+ mock.onGet(props.endpoint).replyOnce(
+ 200,
+ Object.assign({}, job, {
+ status: {
+ group: 'pending',
+ icon: 'status_pending',
+ label: 'pending',
+ text: 'pending',
+ details_path: 'path',
+ },
+ stuck: true,
+ runners: {
+ available: true,
+ online: true,
+ },
+ }),
+ );
+
+ vm = mountComponentWithStore(Component, {
+ props,
+ store,
+ });
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]);
+ expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(
+ "This job is stuck, because you don't have any active runners online with any of these tags assigned to them:",
+ );
+ done();
+ }, 0);
+ });
});
it('does not renders stuck block when there are no runners', done => {
@@ -369,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);
});
});
@@ -418,10 +534,11 @@ describe('Job App ', () => {
vm.$store.state.trace = 'Update';
setTimeout(() => {
- expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).not.toContain('Update');
- expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).toContain(
- 'Different',
+ expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).not.toContain(
+ 'Update',
);
+
+ expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).toContain('Different');
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/components/sidebar_spec.js b/spec/javascripts/jobs/components/sidebar_spec.js
index 460a2e1b5da..424092d2d88 100644
--- a/spec/javascripts/jobs/components/sidebar_spec.js
+++ b/spec/javascripts/jobs/components/sidebar_spec.js
@@ -140,10 +140,11 @@ describe('Sidebar details block', () => {
});
describe('while fetching stages', () => {
- it('renders dropdown with More label', () => {
+ it('it does not render dropdown', () => {
+ store.dispatch('requestStages');
vm = mountComponentWithStore(SidebarComponent, { store });
- expect(vm.$el.querySelector('.js-selected-stage').textContent.trim()).toEqual('More');
+ expect(vm.$el.querySelector('.js-selected-stage')).toBeNull();
});
});
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 46a20122ec8..4195d9d3680 100644
--- a/spec/javascripts/jobs/store/getters_spec.js
+++ b/spec/javascripts/jobs/store/getters_spec.js
@@ -175,43 +175,37 @@ describe('Job Store Getters', () => {
});
});
- describe('isJobStuck', () => {
- describe('when job is pending and runners are not available', () => {
+ describe('hasRunnersForProject', () => {
+ describe('with available and offline runners', () => {
it('returns true', () => {
- localState.job.status = {
- group: 'pending',
- };
localState.job.runners = {
- available: false,
+ available: true,
+ online: false,
};
- expect(getters.isJobStuck(localState)).toEqual(true);
+ expect(getters.hasRunnersForProject(localState)).toEqual(true);
});
});
- describe('when job is not pending', () => {
+ describe('with non available runners', () => {
it('returns false', () => {
- localState.job.status = {
- group: 'running',
- };
localState.job.runners = {
available: false,
+ online: false,
};
- expect(getters.isJobStuck(localState)).toEqual(false);
+ expect(getters.hasRunnersForProject(localState)).toEqual(false);
});
});
- describe('when runners are available', () => {
+ describe('with online runners', () => {
it('returns false', () => {
- localState.job.status = {
- group: 'pending',
- };
localState.job.runners = {
- available: true,
+ available: false,
+ online: true,
};
- expect(getters.isJobStuck(localState)).toEqual(false);
+ expect(getters.hasRunnersForProject(localState)).toEqual(false);
});
});
});
diff --git a/spec/javascripts/jobs/store/mutations_spec.js b/spec/javascripts/jobs/store/mutations_spec.js
index 4230a7c42cf..d7908efcf13 100644
--- a/spec/javascripts/jobs/store/mutations_spec.js
+++ b/spec/javascripts/jobs/store/mutations_spec.js
@@ -124,8 +124,8 @@ describe('Jobs Store Mutations', () => {
expect(stateCopy.job).toEqual({ id: 1312321 });
});
- it('sets selectedStage when the selectedStage is More', () => {
- expect(stateCopy.selectedStage).toEqual('More');
+ it('sets selectedStage when the selectedStage is empty', () => {
+ expect(stateCopy.selectedStage).toEqual('');
mutations[types.RECEIVE_JOB_SUCCESS](stateCopy, { id: 1312321, stage: 'deploy' });
expect(stateCopy.selectedStage).toEqual('deploy');
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/datefix_spec.js b/spec/javascripts/lib/utils/datefix_spec.js
deleted file mode 100644
index a9f3abcf2a4..00000000000
--- a/spec/javascripts/lib/utils/datefix_spec.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { pad, pikadayToString } from '~/lib/utils/datefix';
-
-describe('datefix', () => {
- describe('pad', () => {
- it('should add a 0 when length is smaller than 2', () => {
- expect(pad(2)).toEqual('02');
- });
-
- it('should not add a zero when lenght matches the default', () => {
- expect(pad(12)).toEqual('12');
- });
-
- it('should add a 0 when lenght is smaller than the provided', () => {
- expect(pad(12, 3)).toEqual('012');
- });
- });
-
- describe('parsePikadayDate', () => {
- // removed because of https://gitlab.com/gitlab-org/gitlab-ce/issues/39834
- });
-
- describe('pikadayToString', () => {
- it('should format a UTC date into yyyy-mm-dd format', () => {
- expect(pikadayToString(new Date('2020-01-29:00:00'))).toEqual('2020-01-29');
- });
- });
-});
diff --git a/spec/javascripts/datetime_utility_spec.js b/spec/javascripts/lib/utils/datetime_utility_spec.js
index 9fedbcc4c25..bebe76f76c5 100644
--- a/spec/javascripts/datetime_utility_spec.js
+++ b/spec/javascripts/lib/utils/datetime_utility_spec.js
@@ -192,3 +192,187 @@ describe('formatTime', () => {
});
});
});
+
+describe('datefix', () => {
+ describe('pad', () => {
+ it('should add a 0 when length is smaller than 2', () => {
+ expect(datetimeUtility.pad(2)).toEqual('02');
+ });
+
+ it('should not add a zero when length matches the default', () => {
+ expect(datetimeUtility.pad(12)).toEqual('12');
+ });
+
+ it('should add a 0 when length is smaller than the provided', () => {
+ expect(datetimeUtility.pad(12, 3)).toEqual('012');
+ });
+ });
+
+ describe('parsePikadayDate', () => {
+ // removed because of https://gitlab.com/gitlab-org/gitlab-ce/issues/39834
+ });
+
+ describe('pikadayToString', () => {
+ it('should format a UTC date into yyyy-mm-dd format', () => {
+ expect(datetimeUtility.pikadayToString(new Date('2020-01-29:00:00'))).toEqual('2020-01-29');
+ });
+ });
+});
+
+describe('prettyTime methods', () => {
+ const assertTimeUnits = (obj, minutes, hours, days, weeks) => {
+ expect(obj.minutes).toBe(minutes);
+ expect(obj.hours).toBe(hours);
+ expect(obj.days).toBe(days);
+ expect(obj.weeks).toBe(weeks);
+ };
+
+ describe('parseSeconds', () => {
+ it('should correctly parse a negative value', () => {
+ const zeroSeconds = datetimeUtility.parseSeconds(-1000);
+
+ assertTimeUnits(zeroSeconds, 16, 0, 0, 0);
+ });
+
+ it('should correctly parse a zero value', () => {
+ const zeroSeconds = datetimeUtility.parseSeconds(0);
+
+ assertTimeUnits(zeroSeconds, 0, 0, 0, 0);
+ });
+
+ it('should correctly parse a small non-zero second values', () => {
+ const subOneMinute = datetimeUtility.parseSeconds(10);
+ const aboveOneMinute = datetimeUtility.parseSeconds(100);
+ const manyMinutes = datetimeUtility.parseSeconds(1000);
+
+ assertTimeUnits(subOneMinute, 0, 0, 0, 0);
+ assertTimeUnits(aboveOneMinute, 1, 0, 0, 0);
+ assertTimeUnits(manyMinutes, 16, 0, 0, 0);
+ });
+
+ it('should correctly parse large second values', () => {
+ const aboveOneHour = datetimeUtility.parseSeconds(4800);
+ const aboveOneDay = datetimeUtility.parseSeconds(110000);
+ const aboveOneWeek = datetimeUtility.parseSeconds(25000000);
+
+ assertTimeUnits(aboveOneHour, 20, 1, 0, 0);
+ assertTimeUnits(aboveOneDay, 33, 6, 3, 0);
+ assertTimeUnits(aboveOneWeek, 26, 0, 3, 173);
+ });
+
+ it('should correctly accept a custom param for hoursPerDay', () => {
+ const config = { hoursPerDay: 24 };
+
+ const aboveOneHour = datetimeUtility.parseSeconds(4800, config);
+ const aboveOneDay = datetimeUtility.parseSeconds(110000, config);
+ const aboveOneWeek = datetimeUtility.parseSeconds(25000000, config);
+
+ assertTimeUnits(aboveOneHour, 20, 1, 0, 0);
+ assertTimeUnits(aboveOneDay, 33, 6, 1, 0);
+ assertTimeUnits(aboveOneWeek, 26, 8, 4, 57);
+ });
+
+ it('should correctly accept a custom param for daysPerWeek', () => {
+ const config = { daysPerWeek: 7 };
+
+ const aboveOneHour = datetimeUtility.parseSeconds(4800, config);
+ const aboveOneDay = datetimeUtility.parseSeconds(110000, config);
+ const aboveOneWeek = datetimeUtility.parseSeconds(25000000, config);
+
+ assertTimeUnits(aboveOneHour, 20, 1, 0, 0);
+ assertTimeUnits(aboveOneDay, 33, 6, 3, 0);
+ assertTimeUnits(aboveOneWeek, 26, 0, 0, 124);
+ });
+
+ it('should correctly accept custom params for daysPerWeek and hoursPerDay', () => {
+ const config = { daysPerWeek: 55, hoursPerDay: 14 };
+
+ const aboveOneHour = datetimeUtility.parseSeconds(4800, config);
+ const aboveOneDay = datetimeUtility.parseSeconds(110000, config);
+ const aboveOneWeek = datetimeUtility.parseSeconds(25000000, config);
+
+ assertTimeUnits(aboveOneHour, 20, 1, 0, 0);
+ assertTimeUnits(aboveOneDay, 33, 2, 2, 0);
+ assertTimeUnits(aboveOneWeek, 26, 0, 1, 9);
+ });
+ });
+
+ describe('stringifyTime', () => {
+ it('should stringify values with all non-zero units', () => {
+ const timeObject = {
+ weeks: 1,
+ days: 4,
+ hours: 7,
+ minutes: 20,
+ };
+
+ const timeString = datetimeUtility.stringifyTime(timeObject);
+
+ expect(timeString).toBe('1w 4d 7h 20m');
+ });
+
+ it('should stringify values with some non-zero units', () => {
+ const timeObject = {
+ weeks: 0,
+ days: 4,
+ hours: 0,
+ minutes: 20,
+ };
+
+ const timeString = datetimeUtility.stringifyTime(timeObject);
+
+ expect(timeString).toBe('4d 20m');
+ });
+
+ it('should stringify values with no non-zero units', () => {
+ const timeObject = {
+ weeks: 0,
+ days: 0,
+ hours: 0,
+ minutes: 0,
+ };
+
+ const timeString = datetimeUtility.stringifyTime(timeObject);
+
+ 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', () => {
+ it('should abbreviate stringified times for weeks', () => {
+ const fullTimeString = '1w 3d 4h 5m';
+
+ expect(datetimeUtility.abbreviateTime(fullTimeString)).toBe('1w');
+ });
+
+ it('should abbreviate stringified times for non-weeks', () => {
+ const fullTimeString = '0w 3d 4h 5m';
+
+ expect(datetimeUtility.abbreviateTime(fullTimeString)).toBe('3d');
+ });
+ });
+});
+
+describe('calculateRemainingMilliseconds', () => {
+ beforeEach(() => {
+ spyOn(Date, 'now').and.callFake(() => new Date('2063-04-04T00:42:00Z').getTime());
+ });
+
+ it('calculates the remaining time for a given end date', () => {
+ const milliseconds = datetimeUtility.calculateRemainingMilliseconds('2063-04-04T01:44:03Z');
+
+ expect(milliseconds).toBe(3723000);
+ });
+
+ it('returns 0 if the end date has passed', () => {
+ const milliseconds = datetimeUtility.calculateRemainingMilliseconds('2063-04-03T00:00:00Z');
+
+ expect(milliseconds).toBe(0);
+ });
+});
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_markdown_spec.js b/spec/javascripts/lib/utils/text_markdown_spec.js
index bb7a29fe30a..f71d27eb4e4 100644
--- a/spec/javascripts/lib/utils/text_markdown_spec.js
+++ b/spec/javascripts/lib/utils/text_markdown_spec.js
@@ -86,6 +86,29 @@ describe('init markdown', () => {
expect(textArea.value).toEqual(`${initialValue}* `);
});
+
+ it('places the cursor inside the tags', () => {
+ const start = 'lorem ';
+ const end = ' ipsum';
+ const tag = '*';
+
+ textArea.value = `${start}${end}`;
+ textArea.setSelectionRange(start.length, start.length);
+
+ insertMarkdownText({
+ textArea,
+ text: textArea.value,
+ tag,
+ blockTag: null,
+ selected: '',
+ wrap: true,
+ });
+
+ expect(textArea.value).toEqual(`${start}**${end}`);
+
+ // cursor placement should be between tags
+ expect(textArea.selectionStart).toBe(start.length + tag.length);
+ });
});
describe('with selection', () => {
@@ -98,16 +121,22 @@ describe('init markdown', () => {
});
it('applies the tag to the selected value', () => {
+ const selectedIndex = text.indexOf(selected);
+ const tag = '*';
+
insertMarkdownText({
textArea,
text: textArea.value,
- tag: '*',
+ tag,
blockTag: null,
selected,
wrap: true,
});
expect(textArea.value).toEqual(text.replace(selected, `*${selected}*`));
+
+ // cursor placement should be after selection + 2 tag lengths
+ expect(textArea.selectionStart).toBe(selectedIndex + selected.length + 2 * tag.length);
});
it('replaces the placeholder in the tag', () => {
@@ -166,6 +195,33 @@ describe('init markdown', () => {
expect(textArea.selectionStart).toEqual(expectedText.lastIndexOf(select));
expect(textArea.selectionEnd).toEqual(expectedText.lastIndexOf(select) + select.length);
});
+
+ it('should support selected urls', () => {
+ const expectedUrl = 'http://www.gitlab.com';
+ const expectedSelectionText = 'text';
+ const expectedText = `text [${expectedSelectionText}](${expectedUrl}) text`;
+ const initialValue = `text ${expectedUrl} text`;
+
+ textArea.value = initialValue;
+ const selectedIndex = initialValue.indexOf(expectedUrl);
+ textArea.setSelectionRange(selectedIndex, selectedIndex + expectedUrl.length);
+
+ insertMarkdownText({
+ textArea,
+ text: textArea.value,
+ tag,
+ blockTag: null,
+ selected: expectedUrl,
+ wrap: false,
+ select,
+ });
+
+ expect(textArea.value).toEqual(expectedText);
+ expect(textArea.selectionStart).toEqual(expectedText.indexOf(expectedSelectionText, 1));
+ expect(textArea.selectionEnd).toEqual(
+ expectedText.indexOf(expectedSelectionText, 1) + expectedSelectionText.length,
+ );
+ });
});
});
});
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/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js
index a3477c5f8c6..565b87de248 100644
--- a/spec/javascripts/monitoring/dashboard_spec.js
+++ b/spec/javascripts/monitoring/dashboard_spec.js
@@ -113,6 +113,22 @@ describe('Dashboard', () => {
});
});
+ it('hides the dropdown list when there is no environments', done => {
+ const component = new DashboardComponent({
+ el: document.querySelector('.prometheus-graphs'),
+ propsData: { ...propsData, hasMetrics: true, showPanels: false },
+ });
+
+ component.store.storeEnvironmentsData([]);
+
+ setTimeout(() => {
+ const dropdownMenuEnvironments = component.$el.querySelectorAll('.dropdown-menu ul');
+
+ expect(dropdownMenuEnvironments.length).toEqual(0);
+ done();
+ });
+ });
+
it('renders the dropdown with a single is-active element', done => {
const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
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_actions_spec.js b/spec/javascripts/pipelines/pipelines_actions_spec.js
index b5c62178642..a7dcd532f4f 100644
--- a/spec/javascripts/pipelines/pipelines_actions_spec.js
+++ b/spec/javascripts/pipelines/pipelines_actions_spec.js
@@ -62,9 +62,13 @@ describe('Pipelines Actions dropdown', () => {
);
};
- beforeEach(() => {
+ beforeEach(done => {
spyOn(Date, 'now').and.callFake(() => new Date('2063-04-04T00:42:00Z').getTime());
vm = mountComponent(Component, { actions: [scheduledJobAction, expiredJobAction] });
+
+ Vue.nextTick()
+ .then(done)
+ .catch(done.fail);
});
it('emits postAction event after confirming', () => {
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/pretty_time_spec.js b/spec/javascripts/pretty_time_spec.js
deleted file mode 100644
index 158cd76dd13..00000000000
--- a/spec/javascripts/pretty_time_spec.js
+++ /dev/null
@@ -1,135 +0,0 @@
-import { parseSeconds, abbreviateTime, stringifyTime } from '~/lib/utils/pretty_time';
-
-function assertTimeUnits(obj, minutes, hours, days, weeks) {
- expect(obj.minutes).toBe(minutes);
- expect(obj.hours).toBe(hours);
- expect(obj.days).toBe(days);
- expect(obj.weeks).toBe(weeks);
-}
-
-describe('prettyTime methods', () => {
- describe('parseSeconds', () => {
- it('should correctly parse a negative value', () => {
- const zeroSeconds = parseSeconds(-1000);
-
- assertTimeUnits(zeroSeconds, 16, 0, 0, 0);
- });
-
- it('should correctly parse a zero value', () => {
- const zeroSeconds = parseSeconds(0);
-
- assertTimeUnits(zeroSeconds, 0, 0, 0, 0);
- });
-
- it('should correctly parse a small non-zero second values', () => {
- const subOneMinute = parseSeconds(10);
- const aboveOneMinute = parseSeconds(100);
- const manyMinutes = parseSeconds(1000);
-
- assertTimeUnits(subOneMinute, 0, 0, 0, 0);
- assertTimeUnits(aboveOneMinute, 1, 0, 0, 0);
- assertTimeUnits(manyMinutes, 16, 0, 0, 0);
- });
-
- it('should correctly parse large second values', () => {
- const aboveOneHour = parseSeconds(4800);
- const aboveOneDay = parseSeconds(110000);
- const aboveOneWeek = parseSeconds(25000000);
-
- assertTimeUnits(aboveOneHour, 20, 1, 0, 0);
- assertTimeUnits(aboveOneDay, 33, 6, 3, 0);
- assertTimeUnits(aboveOneWeek, 26, 0, 3, 173);
- });
-
- it('should correctly accept a custom param for hoursPerDay', () => {
- const config = { hoursPerDay: 24 };
-
- const aboveOneHour = parseSeconds(4800, config);
- const aboveOneDay = parseSeconds(110000, config);
- const aboveOneWeek = parseSeconds(25000000, config);
-
- assertTimeUnits(aboveOneHour, 20, 1, 0, 0);
- assertTimeUnits(aboveOneDay, 33, 6, 1, 0);
- assertTimeUnits(aboveOneWeek, 26, 8, 4, 57);
- });
-
- it('should correctly accept a custom param for daysPerWeek', () => {
- const config = { daysPerWeek: 7 };
-
- const aboveOneHour = parseSeconds(4800, config);
- const aboveOneDay = parseSeconds(110000, config);
- const aboveOneWeek = parseSeconds(25000000, config);
-
- assertTimeUnits(aboveOneHour, 20, 1, 0, 0);
- assertTimeUnits(aboveOneDay, 33, 6, 3, 0);
- assertTimeUnits(aboveOneWeek, 26, 0, 0, 124);
- });
-
- it('should correctly accept custom params for daysPerWeek and hoursPerDay', () => {
- const config = { daysPerWeek: 55, hoursPerDay: 14 };
-
- const aboveOneHour = parseSeconds(4800, config);
- const aboveOneDay = parseSeconds(110000, config);
- const aboveOneWeek = parseSeconds(25000000, config);
-
- assertTimeUnits(aboveOneHour, 20, 1, 0, 0);
- assertTimeUnits(aboveOneDay, 33, 2, 2, 0);
- assertTimeUnits(aboveOneWeek, 26, 0, 1, 9);
- });
- });
-
- describe('stringifyTime', () => {
- it('should stringify values with all non-zero units', () => {
- const timeObject = {
- weeks: 1,
- days: 4,
- hours: 7,
- minutes: 20,
- };
-
- const timeString = stringifyTime(timeObject);
-
- expect(timeString).toBe('1w 4d 7h 20m');
- });
-
- it('should stringify values with some non-zero units', () => {
- const timeObject = {
- weeks: 0,
- days: 4,
- hours: 0,
- minutes: 20,
- };
-
- const timeString = stringifyTime(timeObject);
-
- expect(timeString).toBe('4d 20m');
- });
-
- it('should stringify values with no non-zero units', () => {
- const timeObject = {
- weeks: 0,
- days: 0,
- hours: 0,
- minutes: 0,
- };
-
- const timeString = stringifyTime(timeObject);
-
- expect(timeString).toBe('0m');
- });
- });
-
- describe('abbreviateTime', () => {
- it('should abbreviate stringified times for weeks', () => {
- const fullTimeString = '1w 3d 4h 5m';
-
- expect(abbreviateTime(fullTimeString)).toBe('1w');
- });
-
- it('should abbreviate stringified times for non-weeks', () => {
- const fullTimeString = '0w 3d 4h 5m';
-
- expect(abbreviateTime(fullTimeString)).toBe('3d');
- });
- });
-});
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/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index 7530fd2a43b..7a4ca587313 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-var, one-var, no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign, object-shorthand, prefer-template, vars-on-top */
+/* eslint-disable no-var, one-var, no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign, object-shorthand, vars-on-top */
import $ from 'jquery';
import '~/gl_dropdown';
@@ -109,16 +109,16 @@ describe('Search autocomplete dropdown', () => {
assertLinks = function(list, issuesPath, mrsPath) {
if (issuesPath) {
- const issuesAssignedToMeLink = `a[href="${issuesPath}/?assignee_id=${userId}"]`;
- const issuesIHaveCreatedLink = `a[href="${issuesPath}/?author_id=${userId}"]`;
+ const issuesAssignedToMeLink = `a[href="${issuesPath}/?assignee_username=${userName}"]`;
+ const issuesIHaveCreatedLink = `a[href="${issuesPath}/?author_username=${userName}"]`;
expect(list.find(issuesAssignedToMeLink).length).toBe(1);
expect(list.find(issuesAssignedToMeLink).text()).toBe('Issues assigned to me');
expect(list.find(issuesIHaveCreatedLink).length).toBe(1);
expect(list.find(issuesIHaveCreatedLink).text()).toBe("Issues I've created");
}
- const mrsAssignedToMeLink = `a[href="${mrsPath}/?assignee_id=${userId}"]`;
- const mrsIHaveCreatedLink = `a[href="${mrsPath}/?author_id=${userId}"]`;
+ const mrsAssignedToMeLink = `a[href="${mrsPath}/?assignee_username=${userName}"]`;
+ const mrsIHaveCreatedLink = `a[href="${mrsPath}/?author_username=${userName}"]`;
expect(list.find(mrsAssignedToMeLink).length).toBe(1);
expect(list.find(mrsAssignedToMeLink).text()).toBe('Merge requests assigned to me');
@@ -186,7 +186,7 @@ describe('Search autocomplete dropdown', () => {
widget.searchInput.val('help');
widget.searchInput.triggerHandler('focus');
list = widget.wrap.find('.dropdown-menu').find('ul');
- link = "a[href='" + projectIssuesPath + '/?assignee_id=' + userId + "']";
+ link = `a[href='${projectIssuesPath}/?assignee_username=${userName}']`;
expect(list.find(link).length).toBe(0);
});
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 ce850bc621e..056b4df8fdc 100644
--- a/spec/javascripts/vue_mr_widget/components/deployment_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/deployment_spec.js
@@ -2,54 +2,48 @@ import Vue from 'vue';
import deploymentComponent from '~/vue_merge_request_widget/components/deployment.vue';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
import { getTimeago } from '~/lib/utils/datetime_utility';
+import mountComponent from '../../helpers/vue_mount_component_helper';
-const deploymentMockData = {
- 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/',
- },
- ],
-};
-const createComponent = () => {
+describe('Deployment component', () => {
const Component = Vue.extend(deploymentComponent);
+ const deploymentMockData = {
+ id: 15,
+ name: 'review/diplo',
+ url: '/root/review-apps/environments/15',
+ stop_url: '/root/review-apps/environments/15/stop',
+ metrics_url: '/root/review-apps/environments/15/deployments/1/metrics',
+ metrics_monitoring_url: '/root/review-apps/environments/15/metrics',
+ external_url: 'http://gitlab.com.',
+ external_url_formatted: 'gitlab',
+ 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/',
+ },
+ ],
+ };
- return new Component({
- el: document.createElement('div'),
- propsData: { deployment: { ...deploymentMockData } },
- });
-};
-
-describe('Deployment component', () => {
let vm;
- beforeEach(() => {
- vm = createComponent();
- });
-
afterEach(() => {
vm.$destroy();
});
- describe('computed', () => {
+ describe('', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, { deployment: { ...deploymentMockData }, showMetrics: true });
+ });
+
describe('deployTimeago', () => {
it('return formatted date', () => {
const readable = getTimeago().format(deploymentMockData.deployed_at);
@@ -111,9 +105,7 @@ describe('Deployment component', () => {
expect(vm.hasDeploymentMeta).toEqual(false);
});
});
- });
- describe('methods', () => {
describe('stopEnvironment', () => {
const url = '/foo/bar';
const returnPromise = () =>
@@ -152,80 +144,113 @@ describe('Deployment component', () => {
expect(MRWidgetService.stopEnvironment).not.toHaveBeenCalled();
});
});
- });
-
- describe('template', () => {
- let el;
-
- beforeEach(() => {
- vm = createComponent(deploymentMockData);
- el = vm.$el;
- });
it('renders deployment name', () => {
- expect(el.querySelector('.js-deploy-meta').getAttribute('href')).toEqual(
+ expect(vm.$el.querySelector('.js-deploy-meta').getAttribute('href')).toEqual(
deploymentMockData.url,
);
- expect(el.querySelector('.js-deploy-meta').innerText).toContain(deploymentMockData.name);
+ expect(vm.$el.querySelector('.js-deploy-meta').innerText).toContain(deploymentMockData.name);
});
it('renders external URL', () => {
- expect(el.querySelector('.js-deploy-url').getAttribute('href')).toEqual(
+ expect(vm.$el.querySelector('.js-deploy-url').getAttribute('href')).toEqual(
deploymentMockData.external_url,
);
- expect(el.querySelector('.js-deploy-url').innerText).toContain('View app');
+ expect(vm.$el.querySelector('.js-deploy-url').innerText).toContain('View app');
});
it('renders stop button', () => {
- expect(el.querySelector('.btn')).not.toBeNull();
+ expect(vm.$el.querySelector('.btn')).not.toBeNull();
});
it('renders deployment time', () => {
- expect(el.querySelector('.js-deploy-time').innerText).toContain(vm.deployTimeago);
+ expect(vm.$el.querySelector('.js-deploy-time').innerText).toContain(vm.deployTimeago);
});
it('renders metrics component', () => {
- expect(el.querySelector('.js-mr-memory-usage')).not.toBeNull();
+ expect(vm.$el.querySelector('.js-mr-memory-usage')).not.toBeNull();
});
});
- describe('with `features.ciEnvironmentsStatusChanges` enabled', () => {
+ describe('with showMetrics enabled', () => {
beforeEach(() => {
- window.gon = window.gon || {};
- window.gon.features = window.gon.features || {};
- window.gon.features.ciEnvironmentsStatusChanges = true;
+ vm = mountComponent(Component, { deployment: { ...deploymentMockData }, showMetrics: true });
+ });
- vm = createComponent(deploymentMockData);
+ it('shows metrics', () => {
+ expect(vm.$el).toContainElement('.js-mr-memory-usage');
});
+ });
- afterEach(() => {
- window.gon.features = {};
+ describe('with showMetrics disabled', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, { deployment: { ...deploymentMockData }, showMetrics: false });
});
- 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();
+ it('hides metrics', () => {
+ expect(vm.$el).not.toContainElement('.js-mr-memory-usage');
});
});
- describe('with `features.ciEnvironmentsStatusChanges` disabled', () => {
+ describe('without changes', () => {
beforeEach(() => {
- window.gon = window.gon || {};
- window.gon.features = window.gon.features || {};
- window.gon.features.ciEnvironmentsStatusChanges = false;
+ delete deploymentMockData.changes;
- vm = createComponent(deploymentMockData);
+ vm = mountComponent(Component, { deployment: { ...deploymentMockData }, showMetrics: true });
});
- 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();
});
});
+
+ describe('deployment status', () => {
+ describe('running', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ deployment: Object.assign({}, deploymentMockData, { status: 'running' }),
+ showMetrics: true,
+ });
+ });
+
+ 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', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ deployment: Object.assign({}, deploymentMockData, { status: 'success' }),
+ showMetrics: true,
+ });
+ });
+
+ it('renders information about finished deployment', () => {
+ expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain('Deployed to');
+ });
+ });
+
+ describe('failed', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ deployment: Object.assign({}, deploymentMockData, { status: 'failed' }),
+ showMetrics: true,
+ });
+ });
+
+ it('renders information about finished deployment', () => {
+ expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain(
+ 'Failed to deploy to',
+ );
+ });
+ });
+ });
});
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 d1a064b9f4d..f72bf627c10 100644
--- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
@@ -189,7 +189,7 @@ describe('mrWidgetOptions', () => {
it('should fetch deployments', done => {
spyOn(vm.service, 'fetchDeployments').and.returnValue(returnPromise([{ id: 1 }]));
- vm.fetchDeployments();
+ vm.fetchPreMergeDeployments();
setTimeout(() => {
expect(vm.service.fetchDeployments).toHaveBeenCalled();
@@ -454,13 +454,10 @@ describe('mrWidgetOptions', () => {
deployed_at: '2017-03-22T22:44:42.258Z',
deployed_at_formatted: 'Mar 22, 2017 10:44pm',
changes,
+ status: 'success',
};
beforeEach(done => {
- window.gon = window.gon || {};
- window.gon.features = window.gon.features || {};
- window.gon.features.ciEnvironmentsStatusChanges = true;
-
vm.mr.deployments.push(
{
...deploymentMockData,
@@ -486,4 +483,192 @@ describe('mrWidgetOptions', () => {
).toEqual(changes.length);
});
});
+
+ describe('pipeline for target branch after merge', () => {
+ describe('with information for target branch pipeline', () => {
+ beforeEach(done => {
+ vm.mr.state = 'merged';
+ vm.mr.mergePipeline = {
+ id: 127,
+ user: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ web_url: 'http://localhost:3000/root',
+ status_tooltip_html: null,
+ path: '/root',
+ },
+ active: true,
+ coverage: null,
+ source: 'push',
+ created_at: '2018-10-22T11:41:35.186Z',
+ updated_at: '2018-10-22T11:41:35.433Z',
+ path: '/root/ci-web-terminal/pipelines/127',
+ flags: {
+ latest: true,
+ stuck: true,
+ auto_devops: false,
+ yaml_errors: false,
+ retryable: false,
+ cancelable: true,
+ failure_reason: false,
+ },
+ details: {
+ status: {
+ icon: 'status_pending',
+ text: 'pending',
+ label: 'pending',
+ group: 'pending',
+ tooltip: 'pending',
+ has_details: true,
+ details_path: '/root/ci-web-terminal/pipelines/127',
+ illustration: null,
+ favicon:
+ '/assets/ci_favicons/favicon_status_pending-5bdf338420e5221ca24353b6bff1c9367189588750632e9a871b7af09ff6a2ae.png',
+ },
+ duration: null,
+ finished_at: null,
+ stages: [
+ {
+ name: 'test',
+ title: 'test: pending',
+ status: {
+ icon: 'status_pending',
+ text: 'pending',
+ label: 'pending',
+ group: 'pending',
+ tooltip: 'pending',
+ has_details: true,
+ details_path: '/root/ci-web-terminal/pipelines/127#test',
+ illustration: null,
+ favicon:
+ '/assets/ci_favicons/favicon_status_pending-5bdf338420e5221ca24353b6bff1c9367189588750632e9a871b7af09ff6a2ae.png',
+ },
+ path: '/root/ci-web-terminal/pipelines/127#test',
+ dropdown_path: '/root/ci-web-terminal/pipelines/127/stage.json?stage=test',
+ },
+ ],
+ artifacts: [],
+ manual_actions: [],
+ scheduled_actions: [],
+ },
+ ref: {
+ name: 'master',
+ path: '/root/ci-web-terminal/commits/master',
+ tag: false,
+ branch: true,
+ },
+ commit: {
+ id: 'aa1939133d373c94879becb79d91828a892ee319',
+ short_id: 'aa193913',
+ title: "Merge branch 'master-test' into 'master'",
+ created_at: '2018-10-22T11:41:33.000Z',
+ parent_ids: [
+ '4622f4dd792468993003caf2e3be978798cbe096',
+ '76598df914cdfe87132d0c3c40f80db9fa9396a4',
+ ],
+ message:
+ "Merge branch 'master-test' into 'master'\n\nUpdate .gitlab-ci.yml\n\nSee merge request root/ci-web-terminal!1",
+ author_name: 'Administrator',
+ author_email: 'admin@example.com',
+ authored_date: '2018-10-22T11:41:33.000Z',
+ committer_name: 'Administrator',
+ committer_email: 'admin@example.com',
+ committed_date: '2018-10-22T11:41:33.000Z',
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ web_url: 'http://localhost:3000/root',
+ status_tooltip_html: null,
+ path: '/root',
+ },
+ author_gravatar_url: null,
+ commit_url:
+ 'http://localhost:3000/root/ci-web-terminal/commit/aa1939133d373c94879becb79d91828a892ee319',
+ commit_path: '/root/ci-web-terminal/commit/aa1939133d373c94879becb79d91828a892ee319',
+ },
+ cancel_path: '/root/ci-web-terminal/pipelines/127/cancel',
+ };
+ vm.$nextTick(done);
+ });
+
+ it('renders pipeline block', () => {
+ expect(vm.$el.querySelector('.js-post-merge-pipeline')).not.toBeNull();
+ });
+
+ 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.$nextTick(done);
+ });
+
+ it('renders post deployment information', () => {
+ expect(vm.$el.querySelector('.js-post-deployment')).not.toBeNull();
+ });
+ });
+ });
+
+ describe('without information for target branch pipeline', () => {
+ beforeEach(done => {
+ vm.mr.state = 'merged';
+
+ vm.$nextTick(done);
+ });
+
+ it('does not render pipeline block', () => {
+ expect(vm.$el.querySelector('.js-post-merge-pipeline')).toBeNull();
+ });
+ });
+
+ describe('when state is not merged', () => {
+ beforeEach(done => {
+ vm.mr.state = 'archived';
+
+ vm.$nextTick(done);
+ });
+
+ it('does not render pipeline block', () => {
+ expect(vm.$el.querySelector('.js-post-merge-pipeline')).toBeNull();
+ });
+
+ it('does not render post deployment information', () => {
+ expect(vm.$el.querySelector('.js-post-deployment')).toBeNull();
+ });
+ });
+ });
});
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/expand_button_spec.js b/spec/javascripts/vue_shared/components/expand_button_spec.js
index 98fee9a74a5..2af4abc299a 100644
--- a/spec/javascripts/vue_shared/components/expand_button_spec.js
+++ b/spec/javascripts/vue_shared/components/expand_button_spec.js
@@ -18,7 +18,7 @@ describe('expand button', () => {
vm.$destroy();
});
- it('renders a collpased button', () => {
+ it('renders a collapsed button', () => {
expect(vm.$children[0].iconTestClass).toEqual('ic-ellipsis_h');
});
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/gl_countdown_spec.js b/spec/javascripts/vue_shared/components/gl_countdown_spec.js
new file mode 100644
index 00000000000..929ffe219f4
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/gl_countdown_spec.js
@@ -0,0 +1,77 @@
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import Vue from 'vue';
+import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
+
+describe('GlCountdown', () => {
+ const Component = Vue.extend(GlCountdown);
+ let vm;
+ let now = '2000-01-01T00:00:00Z';
+
+ beforeEach(() => {
+ spyOn(Date, 'now').and.callFake(() => new Date(now).getTime());
+ jasmine.clock().install();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ jasmine.clock().uninstall();
+ });
+
+ describe('when there is time remaining', () => {
+ beforeEach(done => {
+ vm = mountComponent(Component, {
+ endDateString: '2000-01-01T01:02:03Z',
+ });
+
+ Vue.nextTick()
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('displays remaining time', () => {
+ expect(vm.$el).toContainText('01:02:03');
+ });
+
+ it('updates remaining time', done => {
+ now = '2000-01-01T00:00:01Z';
+ jasmine.clock().tick(1000);
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.$el).toContainText('01:02:02');
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('when there is no time remaining', () => {
+ beforeEach(done => {
+ vm = mountComponent(Component, {
+ endDateString: '1900-01-01T00:00:00Z',
+ });
+
+ Vue.nextTick()
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('displays 00:00:00', () => {
+ expect(vm.$el).toContainText('00:00:00');
+ });
+ });
+
+ describe('when an invalid date is passed', () => {
+ it('throws a validation error', () => {
+ spyOn(Vue.config, 'warnHandler').and.stub();
+ vm = mountComponent(Component, {
+ endDateString: 'this is invalid',
+ });
+
+ expect(Vue.config.warnHandler).toHaveBeenCalledTimes(1);
+ const [errorMessage] = Vue.config.warnHandler.calls.argsFor(0);
+
+ expect(errorMessage).toMatch(/^Invalid prop: .* "endDateString"/);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/javascripts/vue_shared/components/header_ci_component_spec.js
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 026a0c7ea09..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
@@ -11,29 +11,13 @@ describe('collapsedGroupedDatePicker', () => {
});
});
- it('should render toggle sidebar if showToggleSidebar', (done) => {
- expect(vm.$el.querySelector('.issuable-sidebar-header')).toBeDefined();
-
- vm.showToggleSidebar = false;
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.issuable-sidebar-header')).toBeNull();
- done();
- });
- });
-
describe('toggleCollapse events', () => {
- beforeEach((done) => {
+ beforeEach(done => {
spyOn(vm, 'toggleSidebar');
vm.minDate = new Date('07/17/2016');
Vue.nextTick(done);
});
- it('should emit when sidebar is toggled', () => {
- vm.$el.querySelector('.gutter-toggle').click();
-
- expect(vm.toggleSidebar).toHaveBeenCalled();
- });
-
it('should emit when collapsed-calendar-icon is clicked', () => {
vm.$el.querySelector('.sidebar-collapsed-icon').click();
@@ -42,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);
@@ -58,7 +42,7 @@ describe('collapsedGroupedDatePicker', () => {
});
describe('minDate', () => {
- beforeEach((done) => {
+ beforeEach(done => {
vm.minDate = new Date('07/17/2016');
Vue.nextTick(done);
});
@@ -72,7 +56,7 @@ describe('collapsedGroupedDatePicker', () => {
});
describe('maxDate', () => {
- beforeEach((done) => {
+ beforeEach(done => {
vm.maxDate = new Date('07/17/2017');
Vue.nextTick(done);
});
@@ -92,5 +76,11 @@ describe('collapsedGroupedDatePicker', () => {
expect(icons.length).toEqual(1);
expect(icons[0].innerText.trim()).toEqual('None');
});
+
+ it('should have tooltip as `Start and due date`', () => {
+ const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
+
+ expect(icons[0].dataset.originalTitle).toBe('Start and due date');
+ });
});
});
diff --git a/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js b/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js
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/javascripts/vue_shared/directives/tooltip_spec.js b/spec/javascripts/vue_shared/directives/tooltip_spec.js
index 305d2fd5af4..1d516a280b0 100644
--- a/spec/javascripts/vue_shared/directives/tooltip_spec.js
+++ b/spec/javascripts/vue_shared/directives/tooltip_spec.js
@@ -13,24 +13,45 @@ describe('Tooltip directive', () => {
describe('with a single tooltip', () => {
beforeEach(() => {
- const SomeComponent = Vue.extend({
+ setFixtures('<div id="dummy-element"></div>');
+ vm = new Vue({
+ el: '#dummy-element',
directives: {
tooltip,
},
- template: `
- <div
- v-tooltip
- title="foo">
- </div>
- `,
+ data() {
+ return {
+ tooltip: 'some text',
+ };
+ },
+ template: '<div v-tooltip :title="tooltip"></div>',
});
-
- vm = new SomeComponent().$mount();
});
it('should have tooltip plugin applied', () => {
expect($(vm.$el).data('bs.tooltip')).toBeDefined();
});
+
+ it('displays the title as tooltip', () => {
+ $(vm.$el).tooltip('show');
+ const tooltipElement = document.querySelector('.tooltip-inner');
+
+ expect(tooltipElement.innerText).toContain('some text');
+ });
+
+ it('updates a visible tooltip', done => {
+ $(vm.$el).tooltip('show');
+ const tooltipElement = document.querySelector('.tooltip-inner');
+
+ vm.tooltip = 'other text';
+
+ Vue.nextTick()
+ .then(() => {
+ expect(tooltipElement).toContainText('other text');
+ done();
+ })
+ .catch(done.fail);
+ });
});
describe('with multiple tooltips', () => {
diff --git a/spec/lib/api/helpers/custom_validators_spec.rb b/spec/lib/api/helpers/custom_validators_spec.rb
new file mode 100644
index 00000000000..41e6fb47b11
--- /dev/null
+++ b/spec/lib/api/helpers/custom_validators_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+describe API::Helpers::CustomValidators do
+ let(:scope) do
+ Struct.new(:opts) do
+ def full_name(attr_name)
+ attr_name
+ end
+ end
+ end
+
+ describe API::Helpers::CustomValidators::Absence do
+ subject do
+ described_class.new(['test'], {}, false, scope.new)
+ end
+
+ context 'empty param' do
+ it 'does not raise a validation error' do
+ expect_no_validation_error({})
+ end
+ end
+
+ context 'invalid parameters' do
+ it 'should raise a validation error' do
+ expect_validation_error({ 'test' => 'some_value' })
+ end
+ end
+ end
+
+ describe API::Helpers::CustomValidators::IntegerNoneAny do
+ subject do
+ described_class.new(['test'], {}, false, scope.new)
+ end
+
+ context 'valid parameters' do
+ it 'does not raise a validation error' do
+ expect_no_validation_error({ 'test' => 2 })
+ expect_no_validation_error({ 'test' => 100 })
+ expect_no_validation_error({ 'test' => 'None' })
+ expect_no_validation_error({ 'test' => 'Any' })
+ expect_no_validation_error({ 'test' => 'none' })
+ expect_no_validation_error({ 'test' => 'any' })
+ end
+ end
+
+ context 'invalid parameters' do
+ it 'should raise a validation error' do
+ expect_validation_error({ 'test' => 'some_other_string' })
+ end
+ end
+ end
+
+ def expect_no_validation_error(params)
+ expect { validate_test_param!(params) }.not_to raise_error
+ end
+
+ def expect_validation_error(params)
+ expect { validate_test_param!(params) }.to raise_error(Grape::Exceptions::Validation)
+ end
+
+ def validate_test_param!(params)
+ subject.validate_param!('test', params)
+ end
+end
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/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb
index 8947e2ac4fb..e0691aba600 100644
--- a/spec/lib/extracts_path_spec.rb
+++ b/spec/lib/extracts_path_spec.rb
@@ -205,28 +205,18 @@ describe ExtractsPath do
end
describe '#lfs_blob_ids' do
- shared_examples '#lfs_blob_ids' do
- let(:tag) { @project.repository.add_tag(@project.owner, 'my-annotated-tag', 'master', 'test tag') }
- let(:ref) { tag.target }
- let(:params) { { ref: ref, path: 'README.md' } }
+ let(:tag) { @project.repository.add_tag(@project.owner, 'my-annotated-tag', 'master', 'test tag') }
+ let(:ref) { tag.target }
+ let(:params) { { ref: ref, path: 'README.md' } }
- before do
- @project = create(:project, :repository)
- end
-
- it 'handles annotated tags' do
- assign_ref_vars
-
- expect(lfs_blob_ids).to eq([])
- end
+ before do
+ @project = create(:project, :repository)
end
- context 'when gitaly is enabled' do
- it_behaves_like '#lfs_blob_ids'
- end
+ it 'handles annotated tags' do
+ assign_ref_vars
- context 'when gitaly is disabled', :skip_gitaly_mock do
- it_behaves_like '#lfs_blob_ids'
+ expect(lfs_blob_ids).to eq([])
end
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/digest_column_spec.rb b/spec/lib/gitlab/background_migration/digest_column_spec.rb
new file mode 100644
index 00000000000..3e107ac3027
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/digest_column_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::DigestColumn, :migration, schema: 20180913142237 do
+ let(:personal_access_tokens) { table(:personal_access_tokens) }
+ let(:users) { table(:users) }
+
+ subject { described_class.new }
+
+ describe '#perform' do
+ context 'token is not yet hashed' do
+ before do
+ users.create(id: 1, email: 'user@example.com', projects_limit: 10)
+ personal_access_tokens.create!(id: 1, user_id: 1, name: 'pat-01', token: 'token-01')
+ end
+
+ it 'saves token digest' do
+ expect { subject.perform(PersonalAccessToken, :token, :token_digest, 1, 2) }.to(
+ change { PersonalAccessToken.find(1).token_digest }.from(nil).to(Gitlab::CryptoHelper.sha256('token-01')))
+ end
+
+ it 'erases token' do
+ expect { subject.perform(PersonalAccessToken, :token, :token_digest, 1, 2) }.to(
+ change { PersonalAccessToken.find(1).token }.from('token-01').to(nil))
+ end
+ end
+
+ context 'token is already hashed' do
+ before do
+ users.create(id: 1, email: 'user@example.com', projects_limit: 10)
+ personal_access_tokens.create!(id: 1, user_id: 1, name: 'pat-01', token_digest: 'token-digest-01')
+ end
+
+ it 'does not change existing token digest' do
+ expect { subject.perform(PersonalAccessToken, :token, :token_digest, 1, 2) }.not_to(
+ change { PersonalAccessToken.find(1).token_digest })
+ end
+
+ it 'leaves token empty' do
+ expect { subject.perform(PersonalAccessToken, :token, :token_digest, 1, 2) }.not_to(
+ change { PersonalAccessToken.find(1).token }.from(nil))
+ end
+ end
+ 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/background_migration/redact_links_spec.rb b/spec/lib/gitlab/background_migration/redact_links_spec.rb
new file mode 100644
index 00000000000..a40e68069cc
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/redact_links_spec.rb
@@ -0,0 +1,96 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::RedactLinks, :migration, schema: 20181014121030 do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:issues) { table(:issues) }
+ let(:notes) { table(:notes) }
+ let(:snippets) { table(:snippets) }
+ let(:users) { table(:users) }
+ let(:merge_requests) { table(:merge_requests) }
+ let(:namespace) { namespaces.create(name: 'gitlab', path: 'gitlab-org') }
+ let(:project) { projects.create(namespace_id: namespace.id, name: 'foo') }
+ let(:user) { users.create!(email: 'test@example.com', projects_limit: 100, username: 'test') }
+
+ def create_merge_request(id, params)
+ params.merge!(id: id,
+ target_project_id: project.id,
+ target_branch: 'master',
+ source_project_id: project.id,
+ source_branch: 'mr name',
+ title: "mr name#{id}")
+
+ merge_requests.create(params)
+ end
+
+ def create_issue(id, params)
+ params.merge!(id: id, title: "issue#{id}", project_id: project.id)
+
+ issues.create(params)
+ end
+
+ def create_note(id, params)
+ params[:id] = id
+
+ notes.create(params)
+ end
+
+ def create_snippet(id, params)
+ params.merge!(id: id, author_id: user.id)
+
+ snippets.create(params)
+ end
+
+ def create_resource(model, id, params)
+ send("create_#{model.name.underscore}", id, params)
+ end
+
+ shared_examples_for 'redactable resource' do
+ it 'updates only matching texts' do
+ matching_text = 'some text /sent_notifications/00000000000000000000000000000000/unsubscribe more text'
+ redacted_text = 'some text /sent_notifications/REDACTED/unsubscribe more text'
+ create_resource(model, 1, { field => matching_text })
+ create_resource(model, 2, { field => 'not matching text' })
+ create_resource(model, 3, { field => matching_text })
+ create_resource(model, 4, { field => redacted_text })
+ create_resource(model, 5, { field => matching_text })
+
+ expected = { field => 'some text /sent_notifications/REDACTED/unsubscribe more text',
+ "#{field}_html" => nil }
+ expect_any_instance_of("Gitlab::BackgroundMigration::RedactLinks::#{model}".constantize).to receive(:update_columns).with(expected).and_call_original
+
+ subject.perform(model, field, 2, 4)
+
+ expect(model.where(field => matching_text).pluck(:id)).to eq [1, 5]
+ expect(model.find(3).reload[field]).to eq redacted_text
+ end
+ end
+
+ context 'resource is Issue' do
+ it_behaves_like 'redactable resource' do
+ let(:model) { Issue }
+ let(:field) { :description }
+ end
+ end
+
+ context 'resource is Merge Request' do
+ it_behaves_like 'redactable resource' do
+ let(:model) { MergeRequest }
+ let(:field) { :description }
+ end
+ end
+
+ context 'resource is Note' do
+ it_behaves_like 'redactable resource' do
+ let(:model) { Note }
+ let(:field) { :note }
+ end
+ end
+
+ context 'resource is Snippet' do
+ it_behaves_like 'redactable resource' do
+ let(:model) { Snippet }
+ let(:field) { :description }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb
index 4d5081b0a75..e5999a1c509 100644
--- a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb
+++ b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb
@@ -282,6 +282,21 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cache do
expect(pipeline_status.status).to eq(status)
expect(pipeline_status.ref).to eq(ref)
end
+
+ context 'when status is empty string' do
+ before do
+ Gitlab::Redis::Cache.with do |redis|
+ redis.mapped_hmset(cache_key,
+ { sha: sha, status: '', ref: ref })
+ end
+ end
+
+ it 'reads the status as nil' do
+ pipeline_status.load_from_cache
+
+ expect(pipeline_status.status).to eq(nil)
+ end
+ end
end
describe '#has_cache?' do
diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb
index 4df426c54ae..81804ba5c76 100644
--- a/spec/lib/gitlab/checks/change_access_spec.rb
+++ b/spec/lib/gitlab/checks/change_access_spec.rb
@@ -10,13 +10,16 @@ describe Gitlab::Checks::ChangeAccess do
let(:ref) { 'refs/heads/master' }
let(:changes) { { oldrev: oldrev, newrev: newrev, ref: ref } }
let(:protocol) { 'ssh' }
+ let(:timeout) { Gitlab::GitAccess::INTERNAL_TIMEOUT }
+ let(:logger) { Gitlab::Checks::TimedLogger.new(timeout: timeout) }
subject(:change_access) do
described_class.new(
changes,
project: project,
user_access: user_access,
- protocol: protocol
+ protocol: protocol,
+ logger: logger
)
end
@@ -30,6 +33,19 @@ describe Gitlab::Checks::ChangeAccess do
end
end
+ context 'when time limit was reached' do
+ it 'raises a TimeoutError' do
+ logger = Gitlab::Checks::TimedLogger.new(start_time: timeout.ago, timeout: timeout)
+ access = described_class.new(changes,
+ project: project,
+ user_access: user_access,
+ protocol: protocol,
+ logger: logger)
+
+ expect { access.exec }.to raise_error(Gitlab::Checks::TimedLogger::TimeoutError)
+ end
+ end
+
context 'when the user is not allowed to push to the repo' do
it 'raises an error' do
expect(user_access).to receive(:can_do_action?).with(:push_code).and_return(false)
diff --git a/spec/lib/gitlab/checks/lfs_integrity_spec.rb b/spec/lib/gitlab/checks/lfs_integrity_spec.rb
index ec22e3a198e..887ea8fc1e0 100644
--- a/spec/lib/gitlab/checks/lfs_integrity_spec.rb
+++ b/spec/lib/gitlab/checks/lfs_integrity_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe Gitlab::Checks::LfsIntegrity do
include ProjectForksHelper
+ let!(:time_left) { 50 }
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:newrev) do
@@ -15,7 +16,7 @@ describe Gitlab::Checks::LfsIntegrity do
operations.commit_tree('8856a329dd38ca86dfb9ce5aa58a16d88cc119bd', "New LFS objects")
end
- subject { described_class.new(project, newrev) }
+ subject { described_class.new(project, newrev, time_left) }
describe '#objects_missing?' do
let(:blob_object) { repository.blob_at_branch('lfs', 'files/lfs/lfs_object.iso') }
@@ -67,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/checks/timed_logger_spec.rb b/spec/lib/gitlab/checks/timed_logger_spec.rb
new file mode 100644
index 00000000000..0ed3940c038
--- /dev/null
+++ b/spec/lib/gitlab/checks/timed_logger_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Checks::TimedLogger do
+ let!(:timeout) { 50.seconds }
+ let!(:start) { Time.now }
+ let!(:ref) { "bar" }
+ let!(:logger) { described_class.new(start_time: start, timeout: timeout) }
+ let!(:log_messages) do
+ {
+ foo: "Foo message..."
+ }
+ end
+
+ before do
+ logger.append_message("Checking ref: #{ref}")
+ end
+
+ describe '#log_timed' do
+ it 'logs message' do
+ Timecop.freeze(start + 30.seconds) do
+ logger.log_timed(log_messages[:foo], start) { bar_check }
+ end
+
+ expect(logger.full_message).to eq("Checking ref: bar\nFoo message... (30000.0ms)")
+ end
+
+ context 'when time limit was reached' do
+ it 'cancels action' do
+ Timecop.freeze(start + 50.seconds) do
+ expect do
+ logger.log_timed(log_messages[:foo], start) do
+ bar_check
+ end
+ end.to raise_error(described_class::TimeoutError)
+ end
+
+ expect(logger.full_message).to eq("Checking ref: bar\nFoo message... (cancelled)")
+ end
+
+ it 'cancels action with time elapsed if work was performed' do
+ Timecop.freeze(start + 30.seconds) do
+ expect do
+ logger.log_timed(log_messages[:foo], start) do
+ grpc_check
+ end
+ end.to raise_error(described_class::TimeoutError)
+
+ expect(logger.full_message).to eq("Checking ref: bar\nFoo message... (cancelled after 30000.0ms)")
+ end
+ end
+ end
+ end
+
+ def bar_check
+ 2 + 2
+ end
+
+ def grpc_check
+ raise GRPC::DeadlineExceeded
+ end
+end
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/changes_spec.rb b/spec/lib/gitlab/ci/build/policy/changes_spec.rb
index ab401108c84..523d00c1272 100644
--- a/spec/lib/gitlab/ci/build/policy/changes_spec.rb
+++ b/spec/lib/gitlab/ci/build/policy/changes_spec.rb
@@ -49,6 +49,12 @@ describe Gitlab::Ci::Build::Policy::Changes do
expect(policy).to be_satisfied_by(pipeline, seed)
end
+ it 'is satisfied by matching a pattern with a glob' do
+ policy = described_class.new(%w[some/**/*.{rb,txt}])
+
+ expect(policy).to be_satisfied_by(pipeline, seed)
+ end
+
it 'is not satisfied when pattern does not match path' do
policy = described_class.new(%w[some/*.rb])
@@ -61,6 +67,12 @@ describe Gitlab::Ci::Build::Policy::Changes do
expect(policy).not_to be_satisfied_by(pipeline, seed)
end
+ it 'is not satified when pattern with glob does not match' do
+ policy = described_class.new(%w[invalid/*.{md,rake}])
+
+ expect(policy).not_to be_satisfied_by(pipeline, seed)
+ end
+
context 'when pipelines does not run for a branch update' do
before do
pipeline.before_sha = Gitlab::Git::BLANK_SHA
diff --git a/spec/lib/gitlab/ci/build/policy/refs_spec.rb b/spec/lib/gitlab/ci/build/policy/refs_spec.rb
index 7211187e511..553fc0fb9bf 100644
--- a/spec/lib/gitlab/ci/build/policy/refs_spec.rb
+++ b/spec/lib/gitlab/ci/build/policy/refs_spec.rb
@@ -16,7 +16,7 @@ describe Gitlab::Ci::Build::Policy::Refs do
end
end
- context 'when maching tags' do
+ context 'when matching tags' do
context 'when pipeline runs for a tag' do
let(:pipeline) do
build_stubbed(:ci_pipeline, ref: 'feature', tag: true)
@@ -56,10 +56,10 @@ describe Gitlab::Ci::Build::Policy::Refs do
end
end
- context 'when maching a source' do
+ context 'when matching a source' do
let(:pipeline) { build_stubbed(:ci_pipeline, source: :push) }
- it 'is satisifed when provided source keyword matches' do
+ it 'is satisfied when provided source keyword matches' do
expect(described_class.new(%w[pushes]))
.to be_satisfied_by(pipeline)
end
diff --git a/spec/lib/gitlab/ci/build/policy/variables_spec.rb b/spec/lib/gitlab/ci/build/policy/variables_spec.rb
index 2ce858836e3..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)
@@ -54,7 +54,7 @@ describe Gitlab::Ci::Build::Policy::Variables do
expect(policy).not_to be_satisfied_by(pipeline, seed)
end
- it 'allows to evaluate regular secret variables' do
+ it 'allows to evaluate regular CI variables' do
create(:ci_variable, project: project, key: 'SECRET', value: 'my secret')
policy = described_class.new(["$SECRET == 'my secret'"])
diff --git a/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb b/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb
index d48aac15f28..bd1f2c92844 100644
--- a/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb
@@ -8,7 +8,7 @@ describe Gitlab::Ci::Config::Entry::Artifacts do
let(:config) { { paths: %w[public/] } }
describe '#value' do
- it 'returns artifacs configuration' do
+ it 'returns artifacts configuration' do
expect(entry.value).to eq config
end
end
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/policy_spec.rb b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
index bef93fe7af7..83001b7fdd8 100644
--- a/spec/lib/gitlab/ci/config/entry/policy_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
@@ -58,7 +58,7 @@ describe Gitlab::Ci::Config::Entry::Policy do
end
context 'when using complex policy' do
- context 'when specifiying refs policy' do
+ context 'when specifying refs policy' do
let(:config) { { refs: ['master'] } }
it 'is a correct configuraton' do
diff --git a/spec/lib/gitlab/ci/config/entry/reports_spec.rb b/spec/lib/gitlab/ci/config/entry/reports_spec.rb
index 8095a231cf3..38943138cbf 100644
--- a/spec/lib/gitlab/ci/config/entry/reports_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/reports_spec.rb
@@ -19,7 +19,7 @@ describe Gitlab::Ci::Config::Entry::Reports do
shared_examples 'a valid entry' do |keyword, file|
describe '#value' do
- it 'returns artifacs configuration' do
+ it 'returns artifacts configuration' do
expect(entry.value).to eq({ "#{keyword}": [file] } )
end
end
@@ -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/variables/collection/item_spec.rb b/spec/lib/gitlab/ci/variables/collection/item_spec.rb
index 46874662edd..e1e0582cd11 100644
--- a/spec/lib/gitlab/ci/variables/collection/item_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection/item_spec.rb
@@ -36,7 +36,7 @@ describe Gitlab::Ci::Variables::Collection::Item do
shared_examples 'raises error for invalid type' do
it do
expect { described_class.new(key: variable_key, value: variable_value) }
- .to raise_error ArgumentError, /`value` must be of type String, while it was:/
+ .to raise_error ArgumentError, /`#{variable_key}` must be of type String, while it was:/
end
end
@@ -46,7 +46,7 @@ describe Gitlab::Ci::Variables::Collection::Item do
let(:variable_value) { nil }
let(:expected_value) { nil }
- it_behaves_like 'creates variable'
+ it_behaves_like 'raises error for invalid type'
end
context "when it's an empty string" do
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_collection/merge_request_diff_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
index 4578da70bfc..fbcf515281e 100644
--- a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
+++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
@@ -37,17 +37,7 @@ describe Gitlab::Diff::FileCollection::MergeRequestDiff do
let(:stub_path) { '.gitignore' }
end
- shared_examples 'initializes a DiffCollection' do
- it 'returns a valid instance of a DiffCollection' do
- expect(diff_files).to be_a(Gitlab::Git::DiffCollection)
- end
- end
-
- context 'with Gitaly disabled', :disable_gitaly do
- it_behaves_like 'initializes a DiffCollection'
- end
-
- context 'with Gitaly enabled' do
- it_behaves_like 'initializes a DiffCollection'
+ it 'returns a valid instance of a DiffCollection' do
+ expect(diff_files).to be_a(Gitlab::Git::DiffCollection)
end
end
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..4ba9094b24e 100644
--- a/spec/lib/gitlab/file_detector_spec.rb
+++ b/spec/lib/gitlab/file_detector_spec.rb
@@ -15,7 +15,20 @@ 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)
+ filenames = Gitlab::MarkupHelper::PLAIN_FILENAMES + Gitlab::MarkupHelper::PLAIN_FILENAMES.map(&:upcase)
+ extensions = Gitlab::MarkupHelper::EXTENSIONS + Gitlab::MarkupHelper::EXTENSIONS.map(&:upcase)
+
+ filenames.each do |filename|
+ expect(described_class.type_of(filename)).to eq(:readme)
+
+ extensions.each do |extname|
+ expect(described_class.type_of("#{filename}.#{extname}")).to eq(:readme)
+ end
+ end
+ end
+
+ it 'returns nil for a README.rb file' do
+ expect(described_class.type_of('README.rb')).to be_nil
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/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index b243f0dacae..80dd3dcc58e 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -128,7 +128,7 @@ describe Gitlab::Git::Blob, :seed_helper do
end
end
- shared_examples 'finding blobs by ID' do
+ describe '.raw' do
let(:raw_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::RubyBlob::ID) }
let(:bad_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::BigCommit::ID) }
@@ -166,16 +166,6 @@ describe Gitlab::Git::Blob, :seed_helper do
end
end
- describe '.raw' do
- context 'when the blob_raw Gitaly feature is enabled' do
- it_behaves_like 'finding blobs by ID'
- end
-
- context 'when the blob_raw Gitaly feature is disabled', :skip_gitaly_mock do
- it_behaves_like 'finding blobs by ID'
- end
- end
-
describe '.batch' do
let(:blob_references) do
[
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index 9ef27081f98..74d542060d5 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -94,7 +94,7 @@ describe Gitlab::Git::Commit, :seed_helper do
context 'body_size less than threshold' do
let(:body_size) { 123 }
- it 'fetches commit message seperately' do
+ it 'fetches commit message separately' do
expect(described_class).to receive(:get_message).with(repository, id)
commit.safe_message
@@ -183,110 +183,100 @@ describe Gitlab::Git::Commit, :seed_helper do
end
end
- shared_examples '.where' do
- context 'path is empty string' do
- subject do
- commits = described_class.where(
- repo: repository,
- ref: 'master',
- path: '',
- limit: 10
- )
-
- commits.map { |c| c.id }
- end
+ context 'path is empty string' do
+ subject do
+ commits = described_class.where(
+ repo: repository,
+ ref: 'master',
+ path: '',
+ limit: 10
+ )
- it 'has 10 elements' do
- expect(subject.size).to eq(10)
- end
- it { is_expected.to include(SeedRepo::EmptyCommit::ID) }
+ commits.map { |c| c.id }
end
- context 'path is nil' do
- subject do
- commits = described_class.where(
- repo: repository,
- ref: 'master',
- path: nil,
- limit: 10
- )
-
- commits.map { |c| c.id }
- end
-
- it 'has 10 elements' do
- expect(subject.size).to eq(10)
- end
- it { is_expected.to include(SeedRepo::EmptyCommit::ID) }
+ it 'has 10 elements' do
+ expect(subject.size).to eq(10)
end
+ it { is_expected.to include(SeedRepo::EmptyCommit::ID) }
+ end
- context 'ref is branch name' do
- subject do
- commits = described_class.where(
- repo: repository,
- ref: 'master',
- path: 'files',
- limit: 3,
- offset: 1
- )
+ context 'path is nil' do
+ subject do
+ commits = described_class.where(
+ repo: repository,
+ ref: 'master',
+ path: nil,
+ limit: 10
+ )
- commits.map { |c| c.id }
- end
+ commits.map { |c| c.id }
+ end
- it 'has 3 elements' do
- expect(subject.size).to eq(3)
- end
- it { is_expected.to include("d14d6c0abdd253381df51a723d58691b2ee1ab08") }
- it { is_expected.not_to include("eb49186cfa5c4338011f5f590fac11bd66c5c631") }
+ it 'has 10 elements' do
+ expect(subject.size).to eq(10)
end
+ it { is_expected.to include(SeedRepo::EmptyCommit::ID) }
+ end
- context 'ref is commit id' do
- subject do
- commits = described_class.where(
- repo: repository,
- ref: "874797c3a73b60d2187ed6e2fcabd289ff75171e",
- path: 'files',
- limit: 3,
- offset: 1
- )
+ context 'ref is branch name' do
+ subject do
+ commits = described_class.where(
+ repo: repository,
+ ref: 'master',
+ path: 'files',
+ limit: 3,
+ offset: 1
+ )
- commits.map { |c| c.id }
- end
+ commits.map { |c| c.id }
+ end
- it 'has 3 elements' do
- expect(subject.size).to eq(3)
- end
- it { is_expected.to include("2f63565e7aac07bcdadb654e253078b727143ec4") }
- it { is_expected.not_to include(SeedRepo::Commit::ID) }
+ it 'has 3 elements' do
+ expect(subject.size).to eq(3)
end
+ it { is_expected.to include("d14d6c0abdd253381df51a723d58691b2ee1ab08") }
+ it { is_expected.not_to include("eb49186cfa5c4338011f5f590fac11bd66c5c631") }
+ end
- context 'ref is tag' do
- subject do
- commits = described_class.where(
- repo: repository,
- ref: 'v1.0.0',
- path: 'files',
- limit: 3,
- offset: 1
- )
+ context 'ref is commit id' do
+ subject do
+ commits = described_class.where(
+ repo: repository,
+ ref: "874797c3a73b60d2187ed6e2fcabd289ff75171e",
+ path: 'files',
+ limit: 3,
+ offset: 1
+ )
- commits.map { |c| c.id }
- end
+ commits.map { |c| c.id }
+ end
- it 'has 3 elements' do
- expect(subject.size).to eq(3)
- end
- it { is_expected.to include("874797c3a73b60d2187ed6e2fcabd289ff75171e") }
- it { is_expected.not_to include(SeedRepo::Commit::ID) }
+ it 'has 3 elements' do
+ expect(subject.size).to eq(3)
end
+ it { is_expected.to include("2f63565e7aac07bcdadb654e253078b727143ec4") }
+ it { is_expected.not_to include(SeedRepo::Commit::ID) }
end
- describe '.where with gitaly' do
- it_should_behave_like '.where'
- end
+ context 'ref is tag' do
+ subject do
+ commits = described_class.where(
+ repo: repository,
+ ref: 'v1.0.0',
+ path: 'files',
+ limit: 3,
+ offset: 1
+ )
- describe '.where without gitaly', :skip_gitaly_mock do
- it_should_behave_like '.where'
+ commits.map { |c| c.id }
+ end
+
+ it 'has 3 elements' do
+ expect(subject.size).to eq(3)
+ end
+ it { is_expected.to include("874797c3a73b60d2187ed6e2fcabd289ff75171e") }
+ it { is_expected.not_to include(SeedRepo::Commit::ID) }
end
describe '.between' do
@@ -508,7 +498,7 @@ describe Gitlab::Git::Commit, :seed_helper do
end
end
- shared_examples '#stats' do
+ describe '#stats' do
subject { commit.stats }
describe '#additions' do
@@ -527,14 +517,6 @@ describe Gitlab::Git::Commit, :seed_helper do
end
end
- describe '#stats with gitaly on' do
- it_should_behave_like '#stats'
- end
-
- describe '#stats with gitaly disabled', :skip_gitaly_mock do
- it_should_behave_like '#stats'
- end
-
describe '#has_zero_stats?' do
it { expect(commit.has_zero_stats?).to eq(false) }
end
@@ -577,25 +559,15 @@ describe Gitlab::Git::Commit, :seed_helper do
commit_ids.map { |id| described_class.get_message(repository, id) }
end
- shared_examples 'getting commit messages' do
- it 'gets commit messages' do
- expect(subject).to contain_exactly(
- "Added contributing guide\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
- "Add submodule\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n"
- )
- end
- end
-
- context 'when Gitaly commit_messages feature is enabled' do
- it_behaves_like 'getting commit messages'
-
- it 'gets messages in one batch', :request_store do
- expect { subject.map(&:itself) }.to change { Gitlab::GitalyClient.get_request_count }.by(1)
- end
+ it 'gets commit messages' do
+ expect(subject).to contain_exactly(
+ "Added contributing guide\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
+ "Add submodule\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n"
+ )
end
- context 'when Gitaly commit_messages feature is disabled', :disable_gitaly do
- it_behaves_like 'getting commit messages'
+ it 'gets messages in one batch', :request_store do
+ expect { subject.map(&:itself) }.to change { Gitlab::GitalyClient.get_request_count }.by(1)
end
end
diff --git a/spec/lib/gitlab/git/lfs_changes_spec.rb b/spec/lib/gitlab/git/lfs_changes_spec.rb
index c5e7ab959b2..d035df7e0c2 100644
--- a/spec/lib/gitlab/git/lfs_changes_spec.rb
+++ b/spec/lib/gitlab/git/lfs_changes_spec.rb
@@ -15,5 +15,9 @@ describe Gitlab::Git::LfsChanges do
it 'limits new_objects using object_limit' do
expect(subject.new_pointers(object_limit: 1)).to eq([])
end
+
+ it 'times out if given a small dynamic timeout' do
+ expect { subject.new_pointers(dynamic_timeout: 0.001) }.to raise_error(GRPC::DeadlineExceeded)
+ end
end
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/tag_spec.rb b/spec/lib/gitlab/git/tag_spec.rb
index 2d9db576a6c..abee2822d77 100644
--- a/spec/lib/gitlab/git/tag_spec.rb
+++ b/spec/lib/gitlab/git/tag_spec.rb
@@ -3,7 +3,7 @@ require "spec_helper"
describe Gitlab::Git::Tag, :seed_helper do
let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
- shared_examples 'Gitlab::Git::Repository#tags' do
+ describe '#tags' do
describe 'first tag' do
let(:tag) { repository.tags.first }
@@ -25,14 +25,6 @@ describe Gitlab::Git::Tag, :seed_helper do
it { expect(repository.tags.size).to eq(SeedRepo::Repo::TAGS.size) }
end
- context 'when Gitaly tags feature is enabled' do
- it_behaves_like 'Gitlab::Git::Repository#tags'
- end
-
- context 'when Gitaly tags feature is disabled', :skip_gitaly_mock do
- it_behaves_like 'Gitlab::Git::Repository#tags'
- end
-
describe '.get_message' do
let(:tag_ids) { %w[f4e6814c3e4e7a0de82a9e7cd20c626cc963a2f8 8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b] }
@@ -40,23 +32,13 @@ describe Gitlab::Git::Tag, :seed_helper do
tag_ids.map { |id| described_class.get_message(repository, id) }
end
- shared_examples 'getting tag messages' do
- it 'gets tag messages' do
- expect(subject[0]).to eq("Release\n")
- expect(subject[1]).to eq("Version 1.1.0\n")
- end
- end
-
- context 'when Gitaly tag_messages feature is enabled' do
- it_behaves_like 'getting tag messages'
-
- it 'gets messages in one batch', :request_store do
- expect { subject.map(&:itself) }.to change { Gitlab::GitalyClient.get_request_count }.by(1)
- end
+ it 'gets tag messages' do
+ expect(subject[0]).to eq("Release\n")
+ expect(subject[1]).to eq("Version 1.1.0\n")
end
- context 'when Gitaly tag_messages feature is disabled', :disable_gitaly do
- it_behaves_like 'getting tag messages'
+ it 'gets messages in one batch', :request_store do
+ expect { subject.map(&:itself) }.to change { Gitlab::GitalyClient.get_request_count }.by(1)
end
end
@@ -68,7 +50,7 @@ describe Gitlab::Git::Tag, :seed_helper do
context 'message_size less than threshold' do
let(:message_size) { 123 }
- it 'fetches tag message seperately' do
+ it 'fetches tag message separately' do
expect(described_class).to receive(:get_message).with(repository, gitaly_tag.id)
tag.message
diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb
index 3792d6bf67b..bec875fb03d 100644
--- a/spec/lib/gitlab/git/tree_spec.rb
+++ b/spec/lib/gitlab/git/tree_spec.rb
@@ -80,18 +80,8 @@ describe Gitlab::Git::Tree, :seed_helper do
end
describe '#where' do
- shared_examples '#where' do
- it 'returns an empty array when called with an invalid ref' do
- expect(described_class.where(repository, 'foobar-does-not-exist')).to eq([])
- end
- end
-
- context 'with gitaly' do
- it_behaves_like '#where'
- end
-
- context 'without gitaly', :skip_gitaly_mock do
- it_behaves_like '#where'
+ it 'returns an empty array when called with an invalid ref' do
+ expect(described_class.where(repository, 'foobar-does-not-exist')).to eq([])
end
end
end
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/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index e7da5565c26..a417ef77c9e 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -934,6 +934,16 @@ describe Gitlab::GitAccess do
# There is still an N+1 query with protected branches
expect { access.check('git-receive-pack', changes) }.not_to exceed_query_limit(control_count).with_threshold(1)
end
+
+ it 'raises TimeoutError when #check_single_change_access raises a timeout error' do
+ message = "Push operation timed out\n\nTiming information for debugging purposes:\nRunning checks for ref: wow"
+
+ expect_next_instance_of(Gitlab::Checks::ChangeAccess) do |check|
+ expect(check).to receive(:exec).and_raise(Gitlab::Checks::TimedLogger::TimeoutError)
+ end
+
+ expect { access.check('git-receive-pack', changes) }.to raise_error(described_class::TimeoutError, message)
+ 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/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb
index 81bcd8c28ed..3ae319456da 100644
--- a/spec/lib/gitlab/gitaly_client_spec.rb
+++ b/spec/lib/gitlab/gitaly_client_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
# We stub Gitaly in `spec/support/gitaly.rb` for other tests. We don't want
# those stubs while testing the GitalyClient itself.
-describe Gitlab::GitalyClient, skip_gitaly_mock: true do
+describe Gitlab::GitalyClient do
describe '.stub_class' do
it 'returns the gRPC health check stub' do
expect(described_class.stub_class(:health_check)).to eq(::Grpc::Health::V1::Health::Stub)
@@ -191,102 +191,13 @@ describe Gitlab::GitalyClient, skip_gitaly_mock: true do
let(:feature_name) { 'my_feature' }
let(:real_feature_name) { "gitaly_#{feature_name}" }
- context 'when Gitaly is disabled' do
- before do
- allow(described_class).to receive(:enabled?).and_return(false)
- end
-
- it 'returns false' do
- expect(described_class.feature_enabled?(feature_name)).to be(false)
- end
- end
-
- context 'when the feature status is DISABLED' do
- let(:feature_status) { Gitlab::GitalyClient::MigrationStatus::DISABLED }
-
- it 'returns false' do
- expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(false)
- end
- end
-
- context 'when the feature_status is OPT_IN' do
- let(:feature_status) { Gitlab::GitalyClient::MigrationStatus::OPT_IN }
-
- context "when the feature flag hasn't been set" do
- it 'returns false' do
- expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(false)
- end
- end
-
- context "when the feature flag is set to disable" do
- before do
- Feature.get(real_feature_name).disable
- end
-
- it 'returns false' do
- expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(false)
- end
- end
-
- context "when the feature flag is set to enable" do
- before do
- Feature.get(real_feature_name).enable
- end
-
- it 'returns true' do
- expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(true)
- end
- end
-
- context "when the feature flag is set to a percentage of time" do
- before do
- Feature.get(real_feature_name).enable_percentage_of_time(70)
- end
-
- it 'bases the result on pseudo-random numbers' do
- expect(Random).to receive(:rand).and_return(0.3)
- expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(true)
-
- expect(Random).to receive(:rand).and_return(0.8)
- expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(false)
- end
- end
-
- context "when a feature is not persisted" do
- it 'returns false when opt_into_all_features is off' do
- allow(Feature).to receive(:persisted?).and_return(false)
- allow(described_class).to receive(:opt_into_all_features?).and_return(false)
-
- expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(false)
- end
-
- it 'returns true when the override is on' do
- allow(Feature).to receive(:persisted?).and_return(false)
- allow(described_class).to receive(:opt_into_all_features?).and_return(true)
-
- expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(true)
- end
- end
+ before do
+ allow(Feature).to receive(:enabled?).and_return(false)
end
- context 'when the feature_status is OPT_OUT' do
- let(:feature_status) { Gitlab::GitalyClient::MigrationStatus::OPT_OUT }
-
- context "when the feature flag hasn't been set" do
- it 'returns true' do
- expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(true)
- end
- end
-
- context "when the feature flag is set to disable" do
- before do
- Feature.get(real_feature_name).disable
- end
-
- it 'returns false' do
- expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(false)
- end
- end
+ it 'returns false' do
+ expect(Feature).to receive(:enabled?).with(real_feature_name)
+ expect(described_class.feature_enabled?(feature_name)).to be(false)
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/identifier_spec.rb b/spec/lib/gitlab/identifier_spec.rb
index 0385dd762c2..1e583f4cee2 100644
--- a/spec/lib/gitlab/identifier_spec.rb
+++ b/spec/lib/gitlab/identifier_spec.rb
@@ -11,11 +11,8 @@ describe Gitlab::Identifier do
describe '#identify' do
context 'without an identifier' do
- it 'identifies the user using a commit' do
- expect(identifier).to receive(:identify_using_commit)
- .with(project, '123')
-
- identifier.identify('', project, '123')
+ it 'returns nil' do
+ expect(identifier.identify('')).to be nil
end
end
@@ -24,7 +21,7 @@ describe Gitlab::Identifier do
expect(identifier).to receive(:identify_using_user)
.with("user-#{user.id}")
- identifier.identify("user-#{user.id}", project, '123')
+ identifier.identify("user-#{user.id}")
end
end
@@ -33,49 +30,11 @@ describe Gitlab::Identifier do
expect(identifier).to receive(:identify_using_ssh_key)
.with("key-#{key.id}")
- identifier.identify("key-#{key.id}", project, '123')
+ identifier.identify("key-#{key.id}")
end
end
end
- describe '#identify_using_commit' do
- it "returns the User for an existing commit author's Email address" do
- commit = double(:commit, author: user, author_email: user.email)
-
- expect(project).to receive(:commit).with('123').and_return(commit)
-
- expect(identifier.identify_using_commit(project, '123')).to eq(user)
- end
-
- it 'returns nil when no user could be found' do
- allow(project).to receive(:commit).with('123').and_return(nil)
-
- expect(identifier.identify_using_commit(project, '123')).to be_nil
- end
-
- it 'returns nil when the commit does not have an author Email' do
- commit = double(:commit, author_email: nil)
-
- expect(project).to receive(:commit).with('123').and_return(commit)
-
- expect(identifier.identify_using_commit(project, '123')).to be_nil
- end
-
- it 'caches the found users per Email' do
- commit = double(:commit, author: user, author_email: user.email)
-
- expect(project).to receive(:commit).with('123').twice.and_return(commit)
-
- 2.times do
- expect(identifier.identify_using_commit(project, '123')).to eq(user)
- end
- end
-
- it 'returns nil if the project & ref are not present' do
- expect(identifier.identify_using_commit(nil, nil)).to be_nil
- end
- end
-
describe '#identify_using_user' do
it 'returns the User for an existing ID in the identifier' do
found = identifier.identify_using_user("user-#{user.id}")
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.json b/spec/lib/gitlab/import_export/project.json
index 3f2281f213f..58949f76bd6 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -2556,7 +2556,7 @@
"merge_request_diff_id": 27,
"relative_order": 0,
"sha": "bb5206fee213d983da88c47f9cf4cc6caf9c66dc",
- "message": "Feature conflcit added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
+ "message": "Feature conflict added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-08-06T08:35:52.000+02:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -3605,7 +3605,7 @@
"merge_request_diff_id": 14,
"relative_order": 8,
"sha": "08f22f255f082689c0d7d39d19205085311542bc",
- "message": "remove emtpy file.(beacase git ignore empty file)\nadd whitespace test file.\n",
+ "message": "remove empty file.(beacase git ignore empty file)\nadd whitespace test file.\n",
"authored_date": "2015-11-13T06:00:16.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
@@ -4290,7 +4290,7 @@
"merge_request_diff_id": 13,
"relative_order": 8,
"sha": "08f22f255f082689c0d7d39d19205085311542bc",
- "message": "remove emtpy file.(beacase git ignore empty file)\nadd whitespace test file.\n",
+ "message": "remove empty file.(beacase git ignore empty file)\nadd whitespace test file.\n",
"authored_date": "2015-11-13T06:00:16.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
@@ -4799,7 +4799,7 @@
"merge_request_diff_id": 12,
"relative_order": 8,
"sha": "08f22f255f082689c0d7d39d19205085311542bc",
- "message": "remove emtpy file.(beacase git ignore empty file)\nadd whitespace test file.\n",
+ "message": "remove empty file.(beacase git ignore empty file)\nadd whitespace test file.\n",
"authored_date": "2015-11-13T06:00:16.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
@@ -5507,7 +5507,7 @@
"merge_request_diff_id": 10,
"relative_order": 8,
"sha": "08f22f255f082689c0d7d39d19205085311542bc",
- "message": "remove emtpy file.(beacase git ignore empty file)\nadd whitespace test file.\n",
+ "message": "remove empty file.(beacase git ignore empty file)\nadd whitespace test file.\n",
"authored_date": "2015-11-13T06:00:16.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
diff --git a/spec/lib/gitlab/import_export/project.light.json b/spec/lib/gitlab/import_export/project.light.json
index ba2248073f5..2971ca0f0f8 100644
--- a/spec/lib/gitlab/import_export/project.light.json
+++ b/spec/lib/gitlab/import_export/project.light.json
@@ -101,6 +101,28 @@
]
}
],
+ "services": [
+ {
+ "id": 100,
+ "title": "JetBrains TeamCity CI",
+ "project_id": 5,
+ "created_at": "2016-06-14T15:01:51.315Z",
+ "updated_at": "2016-06-14T15:01:51.315Z",
+ "active": false,
+ "properties": {},
+ "template": true,
+ "push_events": true,
+ "issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": true,
+ "note_events": true,
+ "job_events": true,
+ "type": "TeamcityService",
+ "category": "ci",
+ "default": false,
+ "wiki_page_events": true
+ }
+ ],
"snippets": [],
"hooks": []
}
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..7171e12a849 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -297,7 +297,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
issues: 1,
labels: 1,
milestones: 1,
- first_issue_labels: 1
+ first_issue_labels: 1,
+ services: 1
context 'project.json file access check' do
it 'does not read a symlink' do
@@ -321,7 +322,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" } })
@@ -382,6 +383,12 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.light.json")
end
+ it 'does not import any templated services' do
+ restored_project_json
+
+ expect(project.services.where(template: true).count).to eq(0)
+ end
+
it 'imports labels' do
create(:group_label, name: 'Another label', group: project.group)
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..39852b7fe29 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,10 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
+ helm init --upgrade
+ for i in $(seq 1 30); do helm version && break; sleep 1s; echo "Retrying ($i)..."; done
helm repo add app-name https://repository.example.com
+ helm repo update
#{helm_install_comand}
EOS
end
@@ -38,7 +44,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 +55,10 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
+ helm init --upgrade
+ for i in $(seq 1 30); do helm version && break; sleep 1s; echo "Retrying ($i)..."; done
helm repo add app-name https://repository.example.com
+ helm repo update
#{helm_install_command}
EOS
end
@@ -66,7 +74,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 +86,8 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
+ helm init --upgrade
+ for i in $(seq 1 30); do helm version && break; sleep 1s; echo "Retrying ($i)..."; done
#{helm_install_command}
EOS
end
@@ -93,7 +102,55 @@ 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 --upgrade
+ for i in $(seq 1 30); do helm version && break; sleep 1s; echo "Retrying ($i)..."; done
+ 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 --upgrade
+ for i in $(seq 1 30); do helm version && break; sleep 1s; echo "Retrying ($i)..."; done
+ 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 +162,10 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
+ helm init --upgrade
+ for i in $(seq 1 30); do helm version && break; sleep 1s; echo "Retrying ($i)..."; done
helm repo add app-name https://repository.example.com
+ helm repo update
#{helm_install_command}
EOS
end
@@ -117,7 +176,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 +188,10 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
+ helm init --upgrade
+ for i in $(seq 1 30); do helm version && break; sleep 1s; echo "Retrying ($i)..."; done
helm repo add app-name https://repository.example.com
+ helm repo update
#{helm_install_command}
EOS
end
@@ -144,7 +205,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..2dd3a570a1d 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.11.0-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..9b201dae417 100644
--- a/spec/lib/gitlab/kubernetes/helm/upgrade_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/upgrade_command_spec.rb
@@ -21,8 +21,9 @@ 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 --upgrade
+ for i in $(seq 1 30); do helm version && break; sleep 1s; echo "Retrying ($i)..."; done
+ 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 +34,9 @@ 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 --upgrade
+ for i in $(seq 1 30); do helm version && break; sleep 1s; echo "Retrying ($i)..."; done
+ 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 +58,10 @@ describe Gitlab::Kubernetes::Helm::UpgradeCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
+ helm init --upgrade
+ for i in $(seq 1 30); do helm version && break; sleep 1s; echo "Retrying ($i)..."; done
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 +73,9 @@ 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 --upgrade
+ for i in $(seq 1 30); do helm version && break; sleep 1s; echo "Retrying ($i)..."; done
+ 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 53c5a4e7c94..3979a43216c 100644
--- a/spec/lib/gitlab/kubernetes/kube_client_spec.rb
+++ b/spec/lib/gitlab/kubernetes/kube_client_spec.rb
@@ -6,104 +6,77 @@ describe Gitlab::Kubernetes::KubeClient do
include KubernetesHelpers
let(:api_url) { 'https://kubernetes.example.com/prefix' }
- let(:api_groups) { ['api', 'apis/rbac.authorization.k8s.io'] }
- let(:api_version) { 'v1' }
let(:kubeclient_options) { { auth_options: { bearer_token: 'xyz' } } }
- let(:client) { described_class.new(api_url, api_groups, api_version, kubeclient_options) }
+ let(:client) { described_class.new(api_url, kubeclient_options) }
before do
stub_kubeclient_discover(api_url)
end
- describe '#hashed_clients' do
- subject { client.hashed_clients }
-
- it 'has keys from api groups' do
- expect(subject.keys).to match_array api_groups
- end
-
- it 'has values of Kubeclient::Client' do
- expect(subject.values).to all(be_an_instance_of Kubeclient::Client)
- end
- end
-
- describe '#clients' do
- subject { client.clients }
-
- it 'is not empty' do
- is_expected.to be_present
- end
-
- it 'is an array of Kubeclient::Client objects' do
- is_expected.to all(be_an_instance_of Kubeclient::Client)
- end
-
- it 'has each API group url' do
- expected_urls = api_groups.map { |group| "#{api_url}/#{group}" }
-
- expect(subject.map(&:api_endpoint).map(&:to_s)).to match_array(expected_urls)
+ shared_examples 'a Kubeclient' do
+ it 'is a Kubeclient::Client' do
+ is_expected.to be_an_instance_of Kubeclient::Client
end
it 'has the kubeclient options' do
- subject.each do |client|
- expect(client.auth_options).to eq({ bearer_token: 'xyz' })
- end
- end
-
- it 'has the api_version' do
- subject.each do |client|
- expect(client.instance_variable_get(:@api_version)).to eq('v1')
- end
+ expect(subject.auth_options).to eq({ bearer_token: 'xyz' })
end
end
describe '#core_client' do
subject { client.core_client }
- it 'is a Kubeclient::Client' do
- is_expected.to be_an_instance_of Kubeclient::Client
- end
+ it_behaves_like 'a Kubeclient'
it 'has the core API endpoint' do
expect(subject.api_endpoint.to_s).to match(%r{\/api\Z})
end
+
+ it 'has the api_version' do
+ expect(subject.instance_variable_get(:@api_version)).to eq('v1')
+ end
end
describe '#rbac_client' do
subject { client.rbac_client }
- it 'is a Kubeclient::Client' do
- is_expected.to be_an_instance_of Kubeclient::Client
- end
+ it_behaves_like 'a Kubeclient'
it 'has the RBAC API group endpoint' do
expect(subject.api_endpoint.to_s).to match(%r{\/apis\/rbac.authorization.k8s.io\Z})
end
+
+ it 'has the api_version' do
+ expect(subject.instance_variable_get(:@api_version)).to eq('v1')
+ end
end
describe '#extensions_client' do
subject { client.extensions_client }
- let(:api_groups) { ['apis/extensions'] }
-
- it 'is a Kubeclient::Client' do
- is_expected.to be_an_instance_of Kubeclient::Client
- end
+ it_behaves_like 'a Kubeclient'
it 'has the extensions API group endpoint' do
expect(subject.api_endpoint.to_s).to match(%r{\/apis\/extensions\Z})
end
+
+ it 'has the api_version' do
+ expect(subject.instance_variable_get(:@api_version)).to eq('v1beta1')
+ end
end
- describe '#discover!' do
- it 'makes a discovery request for each API group' do
- client.discover!
+ describe '#knative_client' do
+ subject { client.knative_client }
- api_groups.each do |api_group|
- discovery_url = api_url + '/' + api_group + '/v1'
- expect(WebMock).to have_requested(:get, discovery_url).once
- end
+ 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
@@ -156,21 +129,12 @@ describe Gitlab::Kubernetes::KubeClient do
it 'responds to the method' do
expect(client).to respond_to method
end
-
- context 'no rbac client' do
- let(:api_groups) { ['api'] }
-
- it 'throws an error' do
- expect { client.public_send(method) }.to raise_error(Module::DelegationError)
- end
- end
end
end
end
describe 'extensions API group' do
let(:api_groups) { ['apis/extensions'] }
- let(:api_version) { 'v1beta1' }
let(:extensions_client) { client.extensions_client }
describe '#get_deployments' do
@@ -181,22 +145,11 @@ describe Gitlab::Kubernetes::KubeClient do
it 'responds to the method' do
expect(client).to respond_to :get_deployments
end
-
- context 'no extensions client' do
- let(:api_groups) { ['api'] }
- let(:api_version) { 'v1' }
-
- it 'throws an error' do
- expect { client.get_deployments }.to raise_error(Module::DelegationError)
- end
- end
end
end
describe 'non-entity methods' do
it 'does not proxy for non-entity methods' do
- expect(client.clients.first).to respond_to :proxy_url
-
expect(client).not_to respond_to :proxy_url
end
@@ -211,14 +164,6 @@ describe Gitlab::Kubernetes::KubeClient do
it 'is delegated to the core client' do
expect(client).to delegate_method(:get_pod_log).to(:core_client)
end
-
- context 'when no core client' do
- let(:api_groups) { ['apis/extensions'] }
-
- it 'throws an error' do
- expect { client.get_pod_log('pod-name') }.to raise_error(Module::DelegationError)
- end
- end
end
describe '#watch_pod_log' do
@@ -227,14 +172,6 @@ describe Gitlab::Kubernetes::KubeClient do
it 'is delegated to the core client' do
expect(client).to delegate_method(:watch_pod_log).to(:core_client)
end
-
- context 'when no core client' do
- let(:api_groups) { ['apis/extensions'] }
-
- it 'throws an error' do
- expect { client.watch_pod_log('pod-name') }.to raise_error(Module::DelegationError)
- end
- end
end
describe 'methods that do not exist on any client' do
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/patch/draw_route_spec.rb b/spec/lib/gitlab/patch/draw_route_spec.rb
new file mode 100644
index 00000000000..4009b903dc3
--- /dev/null
+++ b/spec/lib/gitlab/patch/draw_route_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+describe Gitlab::Patch::DrawRoute do
+ subject do
+ Class.new do
+ include Gitlab::Patch::DrawRoute
+
+ def route_path(route_name)
+ File.expand_path("../../../../#{route_name}", __dir__)
+ end
+ end.new
+ end
+
+ before do
+ allow(subject).to receive(:instance_eval)
+ end
+
+ it 'evaluates CE only route' do
+ subject.draw(:help)
+
+ expect(subject).to have_received(:instance_eval)
+ .with(File.read(subject.route_path('config/routes/help.rb')))
+ .once
+
+ expect(subject).to have_received(:instance_eval)
+ .once
+ end
+end
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..10bf624bbdd
--- /dev/null
+++ b/spec/lib/gitlab/private_commit_email_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::PrivateCommitEmail do
+ let(:hostname) { Gitlab::CurrentSettings.current_application_settings.commit_email_hostname }
+ let(:id) { 1 }
+ let(:valid_email) { "#{id}-foo@#{hostname}" }
+ let(:invalid_email) { "#{id}-foo@users.noreply.bar.com" }
+
+ 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
+ it 'parses user id from email' do
+ expect(described_class.user_id_for_email(valid_email)).to eq(id)
+ end
+
+ it 'returns nil on invalid commit email' do
+ expect(described_class.user_id_for_email(invalid_email)).to be_nil
+ end
+ end
+
+ context '.user_ids_for_email' do
+ it 'returns deduplicated user IDs for each valid email' do
+ result = described_class.user_ids_for_emails([valid_email, valid_email, invalid_email])
+
+ expect(result).to eq([id])
+ end
+
+ it 'returns an empty array with no valid emails' do
+ result = described_class.user_ids_for_emails([invalid_email])
+ expect(result).to eq([])
+ 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/repository_cache_spec.rb b/spec/lib/gitlab/repository_cache_spec.rb
index 741ee12633f..1b9a8b4ab0d 100644
--- a/spec/lib/gitlab/repository_cache_spec.rb
+++ b/spec/lib/gitlab/repository_cache_spec.rb
@@ -4,14 +4,14 @@ describe Gitlab::RepositoryCache do
let(:backend) { double('backend').as_null_object }
let(:project) { create(:project) }
let(:repository) { project.repository }
- let(:namespace) { "#{repository.full_path}:#{project.id}" }
+ let(:namespace) { "project:#{project.id}" }
let(:cache) { described_class.new(repository, backend: backend) }
describe '#cache_key' do
subject { cache.cache_key(:foo) }
it 'includes the namespace' do
- expect(subject).to eq "foo:#{namespace}"
+ expect(subject).to eq "#{namespace}:foo"
end
context 'with a given namespace' do
@@ -22,7 +22,7 @@ describe Gitlab::RepositoryCache do
end
it 'includes the full namespace' do
- expect(subject).to eq "foo:#{namespace}:#{extra_namespace}"
+ expect(subject).to eq "#{namespace}:#{extra_namespace}:foo"
end
end
end
@@ -30,21 +30,21 @@ describe Gitlab::RepositoryCache do
describe '#expire' do
it 'expires the given key from the cache' do
cache.expire(:foo)
- expect(backend).to have_received(:delete).with("foo:#{namespace}")
+ expect(backend).to have_received(:delete).with("#{namespace}:foo")
end
end
describe '#fetch' do
it 'fetches the given key from the cache' do
cache.fetch(:bar)
- expect(backend).to have_received(:fetch).with("bar:#{namespace}")
+ expect(backend).to have_received(:fetch).with("#{namespace}:bar")
end
it 'accepts a block' do
p = -> {}
cache.fetch(:baz, &p)
- expect(backend).to have_received(:fetch).with("baz:#{namespace}", &p)
+ expect(backend).to have_received(:fetch).with("#{namespace}:baz", &p)
end
end
@@ -67,7 +67,7 @@ describe Gitlab::RepositoryCache do
end
it 'caches the value' do
- expect(backend).to receive(:write).with("#{key}:#{namespace}", true)
+ expect(backend).to receive(:write).with("#{namespace}:#{key}", true)
cache.fetch_without_caching_false(key) { true }
end
@@ -83,7 +83,7 @@ describe Gitlab::RepositoryCache do
end
it 'does not cache the value' do
- expect(backend).not_to receive(:write).with("#{key}:#{namespace}", true)
+ expect(backend).not_to receive(:write).with("#{namespace}:#{key}", true)
cache.fetch_without_caching_false(key, &p)
end
@@ -92,7 +92,7 @@ describe Gitlab::RepositoryCache do
context 'when the cached value is truthy' do
before do
- backend.write("#{key}:#{namespace}", true)
+ backend.write("#{namespace}:#{key}", true)
end
it 'returns the cached value' do
@@ -116,7 +116,7 @@ describe Gitlab::RepositoryCache do
context 'when the cached value is falsey' do
before do
- backend.write("#{key}:#{namespace}", false)
+ backend.write("#{namespace}:#{key}", false)
end
it 'returns the result of the block' do
@@ -126,7 +126,7 @@ describe Gitlab::RepositoryCache do
end
it 'writes the truthy value to the cache' do
- expect(backend).to receive(:write).with("#{key}:#{namespace}", 'block result')
+ expect(backend).to receive(:write).with("#{namespace}:#{key}", 'block result')
cache.fetch_without_caching_false(key) { 'block result' }
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/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb
index 624e2add860..8df0facdab3 100644
--- a/spec/lib/gitlab/url_blocker_spec.rb
+++ b/spec/lib/gitlab/url_blocker_spec.rb
@@ -29,6 +29,16 @@ describe Gitlab::UrlBlocker do
expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git', protocols: ['http'])).to be true
end
+ it 'returns true for localhost IPs' do
+ expect(described_class.blocked_url?('https://0.0.0.0/foo/foo.git')).to be true
+ expect(described_class.blocked_url?('https://[::1]/foo/foo.git')).to be true
+ expect(described_class.blocked_url?('https://127.0.0.1/foo/foo.git')).to be true
+ end
+
+ it 'returns true for loopback IP' do
+ expect(described_class.blocked_url?('https://127.0.0.2/foo/foo.git')).to be true
+ end
+
it 'returns true for alternative version of 127.0.0.1 (0177.1)' do
expect(described_class.blocked_url?('https://0177.1:65535/foo/foo.git')).to be true
end
@@ -84,6 +94,16 @@ describe Gitlab::UrlBlocker do
end
end
+ it 'allows localhost endpoints' do
+ expect(described_class).not_to be_blocked_url('http://0.0.0.0', allow_localhost: true)
+ expect(described_class).not_to be_blocked_url('http://localhost', allow_localhost: true)
+ expect(described_class).not_to be_blocked_url('http://127.0.0.1', allow_localhost: true)
+ end
+
+ it 'allows loopback endpoints' do
+ expect(described_class).not_to be_blocked_url('http://127.0.0.2', allow_localhost: true)
+ end
+
it 'allows IPv4 link-local endpoints' do
expect(described_class).not_to be_blocked_url('http://169.254.169.254')
expect(described_class).not_to be_blocked_url('http://169.254.168.100')
@@ -122,7 +142,7 @@ describe Gitlab::UrlBlocker do
end
def stub_domain_resolv(domain, ip)
- allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false)])
+ allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false, ipv4_loopback?: false, ipv6_loopback?: false)])
end
def unstub_domain_resolv
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/enqueue_redact_links_spec.rb b/spec/migrations/enqueue_redact_links_spec.rb
new file mode 100644
index 00000000000..a5da76977b7
--- /dev/null
+++ b/spec/migrations/enqueue_redact_links_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20181014121030_enqueue_redact_links.rb')
+
+describe EnqueueRedactLinks, :migration, :sidekiq do
+ let(:merge_requests) { table(:merge_requests) }
+ let(:issues) { table(:issues) }
+ let(:notes) { table(:notes) }
+ let(:projects) { table(:projects) }
+ let(:namespaces) { table(:namespaces) }
+ let(:snippets) { table(:snippets) }
+ let(:users) { table(:users) }
+ let(:user) { users.create!(email: 'test@example.com', projects_limit: 100, username: 'test') }
+
+ before do
+ stub_const("#{described_class.name}::BATCH_SIZE", 1)
+
+ text = 'some text /sent_notifications/00000000000000000000000000000000/unsubscribe more text'
+ group = namespaces.create!(name: 'gitlab', path: 'gitlab')
+ project = projects.create!(namespace_id: group.id)
+
+ merge_requests.create!(id: 1, target_project_id: project.id, source_project_id: project.id, target_branch: 'feature', source_branch: 'master', description: text)
+ issues.create!(id: 1, description: text)
+ notes.create!(id: 1, note: text)
+ notes.create!(id: 2, note: text)
+ snippets.create!(id: 1, description: text, author_id: user.id)
+ end
+
+ it 'correctly schedules background migrations' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, "Note", "note", 1, 1)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, "Note", "note", 2, 2)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, "Issue", "description", 1, 1)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, "MergeRequest", "description", 1, 1)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, "Snippet", "description", 1, 1)
+ expect(BackgroundMigrationWorker.jobs.size).to eq 5
+ end
+ end
+ end
+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/schedule_digest_personal_access_tokens_spec.rb b/spec/migrations/schedule_digest_personal_access_tokens_spec.rb
new file mode 100644
index 00000000000..6d155f78342
--- /dev/null
+++ b/spec/migrations/schedule_digest_personal_access_tokens_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20180913142237_schedule_digest_personal_access_tokens.rb')
+
+describe ScheduleDigestPersonalAccessTokens, :migration, :sidekiq do
+ let(:personal_access_tokens) { table(:personal_access_tokens) }
+ let(:users) { table(:users) }
+
+ before do
+ stub_const("#{described_class.name}::BATCH_SIZE", 4)
+
+ users.create(id: 1, email: 'user@example.com', projects_limit: 10)
+
+ personal_access_tokens.create!(id: 1, user_id: 1, name: 'pat-01', token: 'token-01')
+ personal_access_tokens.create!(id: 2, user_id: 1, name: 'pat-02', token: 'token-02')
+ personal_access_tokens.create!(id: 3, user_id: 1, name: 'pat-03', token_digest: 'token_digest')
+ personal_access_tokens.create!(id: 4, user_id: 1, name: 'pat-04', token: 'token-04')
+ personal_access_tokens.create!(id: 5, user_id: 1, name: 'pat-05', token: 'token-05')
+ personal_access_tokens.create!(id: 6, user_id: 1, name: 'pat-06', token: 'token-06')
+ end
+
+ it 'correctly schedules background migrations' do
+ Sidekiq::Testing.fake! do
+ migrate!
+
+ expect(described_class::MIGRATION).to(
+ be_scheduled_delayed_migration(
+ 5.minutes, 'PersonalAccessToken', 'token', 'token_digest', 1, 5))
+ expect(described_class::MIGRATION).to(
+ be_scheduled_delayed_migration(
+ 10.minutes, 'PersonalAccessToken', 'token', 'token_digest', 6, 6))
+ expect(BackgroundMigrationWorker.jobs.size).to eq 2
+ end
+ end
+
+ it 'schedules background migrations' do
+ perform_enqueued_jobs do
+ plain_text_token = 'token IS NOT NULL'
+
+ expect(personal_access_tokens.where(plain_text_token).count).to eq 5
+
+ migrate!
+
+ expect(personal_access_tokens.where(plain_text_token).count).to eq 0
+ end
+ end
+end
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/board_group_recent_visit_spec.rb b/spec/models/board_group_recent_visit_spec.rb
new file mode 100644
index 00000000000..59ad4e5417e
--- /dev/null
+++ b/spec/models/board_group_recent_visit_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe BoardGroupRecentVisit do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:board) { create(:board, group: group) }
+
+ describe 'relationships' do
+ it { is_expected.to belong_to(:user) }
+ it { is_expected.to belong_to(:group) }
+ it { is_expected.to belong_to(:board) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:user) }
+ it { is_expected.to validate_presence_of(:group) }
+ it { is_expected.to validate_presence_of(:board) }
+ end
+
+ describe '#visited' do
+ it 'creates a visit if one does not exists' do
+ expect { described_class.visited!(user, board) }.to change(described_class, :count).by(1)
+ end
+
+ shared_examples 'was visited previously' do
+ let!(:visit) { create :board_group_recent_visit, group: board.group, board: board, user: user, updated_at: 7.days.ago }
+
+ it 'updates the timestamp' do
+ Timecop.freeze do
+ described_class.visited!(user, board)
+
+ expect(described_class.count).to eq 1
+ expect(described_class.first.updated_at).to be_like_time(Time.zone.now)
+ end
+ end
+ end
+
+ it_behaves_like 'was visited previously'
+
+ context 'when we try to create a visit that is not unique' do
+ before do
+ expect(described_class).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique, 'record not unique')
+ expect(described_class).to receive(:find_or_create_by).and_return(visit)
+ end
+
+ it_behaves_like 'was visited previously'
+ end
+ end
+
+ describe '#latest' do
+ it 'returns the most recent visited' do
+ board2 = create(:board, group: group)
+ board3 = create(:board, group: group)
+
+ create :board_group_recent_visit, group: board.group, board: board, user: user, updated_at: 7.days.ago
+ create :board_group_recent_visit, group: board2.group, board: board2, user: user, updated_at: 5.days.ago
+ recent = create :board_group_recent_visit, group: board3.group, board: board3, user: user, updated_at: 1.day.ago
+
+ expect(described_class.latest(user, group)).to eq recent
+ end
+ end
+end
diff --git a/spec/models/board_project_recent_visit_spec.rb b/spec/models/board_project_recent_visit_spec.rb
new file mode 100644
index 00000000000..275581945fa
--- /dev/null
+++ b/spec/models/board_project_recent_visit_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe BoardProjectRecentVisit do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:board) { create(:board, project: project) }
+
+ describe 'relationships' do
+ it { is_expected.to belong_to(:user) }
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:board) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:user) }
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:board) }
+ end
+
+ describe '#visited' do
+ it 'creates a visit if one does not exists' do
+ expect { described_class.visited!(user, board) }.to change(described_class, :count).by(1)
+ end
+
+ shared_examples 'was visited previously' do
+ let!(:visit) { create :board_project_recent_visit, project: board.project, board: board, user: user, updated_at: 7.days.ago }
+
+ it 'updates the timestamp' do
+ Timecop.freeze do
+ described_class.visited!(user, board)
+
+ expect(described_class.count).to eq 1
+ expect(described_class.first.updated_at).to be_like_time(Time.zone.now)
+ end
+ end
+ end
+
+ it_behaves_like 'was visited previously'
+
+ context 'when we try to create a visit that is not unique' do
+ before do
+ expect(described_class).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique, 'record not unique')
+ expect(described_class).to receive(:find_or_create_by).and_return(visit)
+ end
+
+ it_behaves_like 'was visited previously'
+ end
+ end
+
+ describe '#latest' do
+ it 'returns the most recent visited' do
+ board2 = create(:board, project: project)
+ board3 = create(:board, project: project)
+
+ create :board_project_recent_visit, project: board.project, board: board, user: user, updated_at: 7.days.ago
+ create :board_project_recent_visit, project: board2.project, board: board2, user: user, updated_at: 5.days.ago
+ recent = create :board_project_recent_visit, project: board3.project, board: board3, user: user, updated_at: 1.day.ago
+
+ expect(described_class.latest(user, project)).to eq recent
+ end
+ end
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index a046541031e..d02c3a5765f 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
@@ -1695,12 +1928,26 @@ describe Ci::Build do
describe '#repo_url' do
subject { build.repo_url }
- it { is_expected.to be_a(String) }
- it { is_expected.to end_with(".git") }
- it { is_expected.to start_with(project.web_url[0..6]) }
- it { is_expected.to include(build.token) }
- it { is_expected.to include('gitlab-ci-token') }
- it { is_expected.to include(project.web_url[7..-1]) }
+ context 'when token is set' do
+ before do
+ build.ensure_token
+ end
+
+ it { is_expected.to be_a(String) }
+ it { is_expected.to end_with(".git") }
+ it { is_expected.to start_with(project.web_url[0..6]) }
+ it { is_expected.to include(build.token) }
+ it { is_expected.to include('gitlab-ci-token') }
+ it { is_expected.to include(project.web_url[7..-1]) }
+ end
+
+ context 'when token is empty' do
+ before do
+ build.token = nil
+ end
+
+ it { is_expected.to be_nil}
+ end
end
describe '#stuck?' do
@@ -1810,7 +2057,8 @@ describe Ci::Build do
end
context 'use from gitlab-ci.yml' do
- let(:pipeline) { create(:ci_pipeline) }
+ let(:project) { create(:project, :repository) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
before do
stub_ci_pipeline_yaml_file(config)
@@ -1852,55 +2100,6 @@ describe Ci::Build do
describe '#variables' do
let(:container_registry_enabled) { false }
- let(:gitlab_version_info) { Gitlab::VersionInfo.parse(Gitlab::VERSION) }
- let(:predefined_variables) do
- [
- { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true },
- { key: 'CI_PIPELINE_URL', value: project.web_url + "/pipelines/#{pipeline.id}", public: true },
- { key: 'CI_JOB_ID', value: build.id.to_s, public: true },
- { key: 'CI_JOB_URL', value: project.web_url + "/-/jobs/#{build.id}", public: true },
- { key: 'CI_JOB_TOKEN', value: build.token, public: false },
- { key: 'CI_BUILD_ID', value: build.id.to_s, public: true },
- { key: 'CI_BUILD_TOKEN', value: build.token, public: false },
- { key: 'CI_REGISTRY_USER', value: 'gitlab-ci-token', public: true },
- { key: 'CI_REGISTRY_PASSWORD', value: build.token, public: false },
- { key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false },
- { key: 'CI', value: 'true', public: true },
- { key: 'GITLAB_CI', value: 'true', public: true },
- { key: 'GITLAB_FEATURES', value: project.licensed_features.join(','), public: true },
- { key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
- { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
- { key: 'CI_SERVER_VERSION_MAJOR', value: gitlab_version_info.major.to_s, public: true },
- { key: 'CI_SERVER_VERSION_MINOR', value: gitlab_version_info.minor.to_s, public: true },
- { key: 'CI_SERVER_VERSION_PATCH', value: gitlab_version_info.patch.to_s, public: true },
- { key: 'CI_SERVER_REVISION', value: Gitlab.revision, public: true },
- { key: 'CI_JOB_NAME', value: 'test', public: true },
- { key: 'CI_JOB_STAGE', value: 'test', public: true },
- { key: 'CI_COMMIT_SHA', value: build.sha, public: true },
- { 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_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 },
- { key: 'CI_BUILD_REF_SLUG', value: build.ref_slug, public: true },
- { key: 'CI_BUILD_NAME', value: 'test', public: true },
- { key: 'CI_BUILD_STAGE', value: 'test', public: true },
- { key: 'CI_PROJECT_ID', value: project.id.to_s, public: true },
- { key: 'CI_PROJECT_NAME', value: project.path, public: true },
- { key: 'CI_PROJECT_PATH', value: project.full_path, public: true },
- { key: 'CI_PROJECT_PATH_SLUG', value: project.full_path_slug, public: true },
- { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true },
- { key: 'CI_PROJECT_URL', value: project.web_url, public: true },
- { key: 'CI_PROJECT_VISIBILITY', value: 'private', public: true },
- { key: 'CI_PIPELINE_IID', value: pipeline.iid.to_s, public: true },
- { key: 'CI_CONFIG_PATH', value: pipeline.ci_yaml_file_path, public: true },
- { key: 'CI_PIPELINE_SOURCE', value: pipeline.source, public: true },
- { key: 'CI_COMMIT_MESSAGE', value: pipeline.git_commit_message, public: true },
- { key: 'CI_COMMIT_TITLE', value: pipeline.git_commit_title, public: true },
- { key: 'CI_COMMIT_DESCRIPTION', value: pipeline.git_commit_description, public: true }
- ]
- end
before do
stub_container_registry_config(enabled: container_registry_enabled, host_port: 'registry.example.com')
@@ -1909,11 +2108,174 @@ describe Ci::Build do
subject { build.variables }
context 'returns variables' do
+ let(:predefined_variables) do
+ [
+ { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true },
+ { key: 'CI_PIPELINE_URL', value: project.web_url + "/pipelines/#{pipeline.id}", public: true },
+ { key: 'CI_JOB_ID', value: build.id.to_s, public: true },
+ { key: 'CI_JOB_URL', value: project.web_url + "/-/jobs/#{build.id}", public: true },
+ { key: 'CI_JOB_TOKEN', value: 'my-token', public: false },
+ { key: 'CI_BUILD_ID', value: build.id.to_s, public: true },
+ { key: 'CI_BUILD_TOKEN', value: 'my-token', public: false },
+ { key: 'CI_REGISTRY_USER', value: 'gitlab-ci-token', public: true },
+ { key: 'CI_REGISTRY_PASSWORD', value: 'my-token', public: false },
+ { key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false },
+ { key: 'CI', value: 'true', public: true },
+ { key: 'GITLAB_CI', value: 'true', public: true },
+ { key: 'GITLAB_FEATURES', value: project.licensed_features.join(','), public: true },
+ { key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
+ { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
+ { key: 'CI_SERVER_VERSION_MAJOR', value: Gitlab.version_info.major.to_s, public: true },
+ { key: 'CI_SERVER_VERSION_MINOR', value: Gitlab.version_info.minor.to_s, public: true },
+ { key: 'CI_SERVER_VERSION_PATCH', value: Gitlab.version_info.patch.to_s, public: true },
+ { key: 'CI_SERVER_REVISION', value: Gitlab.revision, public: true },
+ { key: 'CI_JOB_NAME', value: 'test', public: true },
+ { key: 'CI_JOB_STAGE', value: 'test', public: true },
+ { key: 'CI_COMMIT_SHA', value: build.sha, public: true },
+ { 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 },
+ { key: 'CI_BUILD_REF_SLUG', value: build.ref_slug, public: true },
+ { key: 'CI_BUILD_NAME', value: 'test', public: true },
+ { key: 'CI_BUILD_STAGE', value: 'test', public: true },
+ { key: 'CI_PROJECT_ID', value: project.id.to_s, public: true },
+ { key: 'CI_PROJECT_NAME', value: project.path, public: true },
+ { key: 'CI_PROJECT_PATH', value: project.full_path, public: true },
+ { key: 'CI_PROJECT_PATH_SLUG', value: project.full_path_slug, public: true },
+ { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true },
+ { key: 'CI_PROJECT_URL', value: project.web_url, public: true },
+ { key: 'CI_PROJECT_VISIBILITY', value: 'private', public: true },
+ { key: 'CI_PIPELINE_IID', value: pipeline.iid.to_s, public: true },
+ { key: 'CI_CONFIG_PATH', value: pipeline.ci_yaml_file_path, public: true },
+ { key: 'CI_PIPELINE_SOURCE', value: pipeline.source, public: true },
+ { key: 'CI_COMMIT_MESSAGE', value: pipeline.git_commit_message, public: true },
+ { key: 'CI_COMMIT_TITLE', value: pipeline.git_commit_title, public: true },
+ { key: 'CI_COMMIT_DESCRIPTION', value: pipeline.git_commit_description, public: true }
+ ]
+ end
+
before do
+ build.token = 'my-token'
build.yaml_variables = []
end
it { is_expected.to include(*predefined_variables) }
+
+ context 'when yaml variables are undefined' do
+ let(:pipeline) do
+ create(:ci_pipeline, project: project,
+ sha: project.commit.id,
+ ref: project.default_branch)
+ end
+
+ before do
+ build.yaml_variables = nil
+ end
+
+ context 'use from gitlab-ci.yml' do
+ before do
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ context 'when config is not found' do
+ let(:config) { nil }
+
+ it { is_expected.to include(*predefined_variables) }
+ end
+
+ context 'when config does not have a questioned job' do
+ let(:config) do
+ YAML.dump({
+ test_other: {
+ script: 'Hello World'
+ }
+ })
+ end
+
+ it { is_expected.to include(*predefined_variables) }
+ end
+
+ context 'when config has variables' do
+ let(:config) do
+ YAML.dump({
+ test: {
+ script: 'Hello World',
+ variables: {
+ KEY: 'value'
+ }
+ }
+ })
+ end
+
+ let(:variables) do
+ [{ key: 'KEY', value: 'value', public: true }]
+ end
+
+ it { is_expected.to include(*predefined_variables) }
+ it { is_expected.to include(*variables) }
+ end
+ end
+ end
+
+ describe 'variables ordering' do
+ context 'when variables hierarchy is stubbed' do
+ let(:build_pre_var) { { key: 'build', value: 'value', public: true } }
+ let(:project_pre_var) { { key: 'project', value: 'value', public: true } }
+ let(:pipeline_pre_var) { { key: 'pipeline', value: 'value', public: true } }
+ let(:build_yaml_var) { { key: 'yaml', value: 'value', public: true } }
+
+ before do
+ allow(build).to receive(:predefined_variables) { [build_pre_var] }
+ allow(build).to receive(:yaml_variables) { [build_yaml_var] }
+ allow(build).to receive(:persisted_variables) { [] }
+
+ allow_any_instance_of(Project)
+ .to receive(:predefined_variables) { [project_pre_var] }
+
+ project.variables.create!(key: 'secret', value: 'value')
+
+ allow_any_instance_of(Ci::Pipeline)
+ .to receive(:predefined_variables) { [pipeline_pre_var] }
+ end
+
+ it 'returns variables in order depending on resource hierarchy' do
+ is_expected.to eq(
+ [build_pre_var,
+ project_pre_var,
+ pipeline_pre_var,
+ build_yaml_var,
+ { key: 'secret', value: 'value', public: false }])
+ end
+ end
+
+ context 'when build has environment and user-provided variables' do
+ let(:expected_variables) do
+ predefined_variables.map { |variable| variable.fetch(:key) } +
+ %w[YAML_VARIABLE CI_ENVIRONMENT_NAME CI_ENVIRONMENT_SLUG
+ CI_ENVIRONMENT_URL]
+ end
+
+ before do
+ create(:environment, project: build.project,
+ name: 'staging')
+
+ build.yaml_variables = [{ key: 'YAML_VARIABLE',
+ value: 'var',
+ public: true }]
+ build.environment = 'staging'
+ end
+
+ it 'matches explicit variables ordering' do
+ received_variables = subject.map { |variable| variable.fetch(:key) }
+
+ expect(received_variables).to eq expected_variables
+ end
+ end
+ end
end
context 'when build has user' do
@@ -2027,17 +2389,17 @@ describe Ci::Build do
it { is_expected.to include(tag_variable) }
end
- context 'when secret variable is defined' do
- let(:secret_variable) do
+ context 'when CI variable is defined' do
+ let(:ci_variable) do
{ key: 'SECRET_KEY', value: 'secret_value', public: false }
end
before do
create(:ci_variable,
- secret_variable.slice(:key, :value).merge(project: project))
+ ci_variable.slice(:key, :value).merge(project: project))
end
- it { is_expected.to include(secret_variable) }
+ it { is_expected.to include(ci_variable) }
end
context 'when protected variable is defined' do
@@ -2072,17 +2434,17 @@ describe Ci::Build do
end
end
- context 'when group secret variable is defined' do
- let(:secret_variable) do
+ context 'when group CI variable is defined' do
+ let(:ci_variable) do
{ key: 'SECRET_KEY', value: 'secret_value', public: false }
end
before do
create(:ci_group_variable,
- secret_variable.slice(:key, :value).merge(group: group))
+ ci_variable.slice(:key, :value).merge(group: group))
end
- it { is_expected.to include(secret_variable) }
+ it { is_expected.to include(ci_variable) }
end
context 'when group protected variable is defined' do
@@ -2175,75 +2537,20 @@ describe Ci::Build do
end
before do
- pipeline_schedule.pipelines << pipeline
+ pipeline_schedule.pipelines << pipeline.reload
pipeline_schedule.reload
end
it { is_expected.to include(pipeline_schedule_variable.to_runner_variable) }
end
- context 'when yaml_variables are undefined' do
- let(:pipeline) do
- create(:ci_pipeline, project: project,
- sha: project.commit.id,
- ref: project.default_branch)
- end
-
- before do
- build.yaml_variables = nil
- end
-
- context 'use from gitlab-ci.yml' do
- before do
- stub_ci_pipeline_yaml_file(config)
- end
-
- context 'when config is not found' do
- let(:config) { nil }
-
- it { is_expected.to include(*predefined_variables) }
- end
-
- context 'when config does not have a questioned job' do
- let(:config) do
- YAML.dump({
- test_other: {
- script: 'Hello World'
- }
- })
- end
-
- it { is_expected.to include(*predefined_variables) }
- end
-
- context 'when config has variables' do
- let(:config) do
- YAML.dump({
- test: {
- script: 'Hello World',
- variables: {
- KEY: 'value'
- }
- }
- })
- end
- let(:variables) do
- [{ key: 'KEY', value: 'value', public: true }]
- end
-
- it { is_expected.to include(*predefined_variables) }
- it { is_expected.to include(*variables) }
- end
- end
- end
-
context 'when container registry is enabled' do
let(:container_registry_enabled) { true }
let(:ci_registry) do
- { key: 'CI_REGISTRY', value: 'registry.example.com', public: true }
+ { key: 'CI_REGISTRY', value: 'registry.example.com', public: true }
end
let(:ci_registry_image) do
- { key: 'CI_REGISTRY_IMAGE', value: project.container_registry_url, public: true }
+ { key: 'CI_REGISTRY_IMAGE', value: project.container_registry_url, public: true }
end
context 'and is disabled for project' do
@@ -2341,63 +2648,26 @@ describe Ci::Build do
end
end
- describe 'variables ordering' do
- context 'when variables hierarchy is stubbed' do
- let(:build_pre_var) { { key: 'build', value: 'value', public: true } }
- let(:project_pre_var) { { key: 'project', value: 'value', public: true } }
- let(:pipeline_pre_var) { { key: 'pipeline', value: 'value', public: true } }
- let(:build_yaml_var) { { key: 'yaml', value: 'value', public: true } }
-
- before do
- allow(build).to receive(:predefined_variables) { [build_pre_var] }
- allow(build).to receive(:yaml_variables) { [build_yaml_var] }
- allow(build).to receive(:persisted_variables) { [] }
-
- allow_any_instance_of(Project)
- .to receive(:predefined_variables) { [project_pre_var] }
+ context 'when build is parallelized' do
+ let(:total) { 5 }
+ let(:index) { 3 }
- allow_any_instance_of(Project)
- .to receive(:secret_variables_for)
- .with(ref: 'master', environment: nil) do
- [create(:ci_variable, key: 'secret', value: 'value')]
- end
-
- allow_any_instance_of(Ci::Pipeline)
- .to receive(:predefined_variables) { [pipeline_pre_var] }
- end
-
- it 'returns variables in order depending on resource hierarchy' do
- is_expected.to eq(
- [build_pre_var,
- project_pre_var,
- pipeline_pre_var,
- build_yaml_var,
- { key: 'secret', value: 'value', public: false }])
- end
+ before do
+ build.options[:parallel] = total
+ build.options[:instance] = index
+ build.name = "#{build.name} #{index}/#{total}"
end
- context 'when build has environment and user-provided variables' do
- let(:expected_variables) do
- predefined_variables.map { |variable| variable.fetch(:key) } +
- %w[YAML_VARIABLE CI_ENVIRONMENT_NAME CI_ENVIRONMENT_SLUG
- CI_ENVIRONMENT_URL]
- end
-
- before do
- create(:environment, project: build.project,
- name: 'staging')
-
- build.yaml_variables = [{ key: 'YAML_VARIABLE',
- value: 'var',
- public: true }]
- build.environment = 'staging'
- end
-
- it 'matches explicit variables ordering' do
- received_variables = subject.map { |variable| variable.fetch(:key) }
+ it 'includes CI_NODE_INDEX' do
+ is_expected.to include(
+ { key: 'CI_NODE_INDEX', value: index.to_s, public: true }
+ )
+ end
- expect(received_variables).to eq expected_variables
- end
+ it 'includes correct CI_NODE_TOTAL' do
+ is_expected.to include(
+ { key: 'CI_NODE_TOTAL', value: total.to_s, public: true }
+ )
end
end
@@ -2508,7 +2778,7 @@ describe Ci::Build do
end
describe '#scoped_variables_hash' do
- context 'when overriding secret variables' do
+ context 'when overriding CI variables' do
before do
project.variables.create!(key: 'MY_VAR', value: 'my value 1')
pipeline.variables.create!(key: 'MY_VAR', value: 'my value 2')
@@ -2728,7 +2998,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 +3427,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 +3442,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 +3451,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 +3459,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..b5aa1dcece5 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 }
@@ -34,7 +35,7 @@ describe Clusters::Applications::Prometheus do
describe 'transition to installed' do
let(:project) { create(:project) }
- let(:cluster) { create(:cluster, projects: [project]) }
+ let(:cluster) { create(:cluster, :with_installed_helm, projects: [project]) }
let(:prometheus_service) { double('prometheus_service') }
subject { create(:clusters_applications_prometheus, :installing, cluster: cluster) }
@@ -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 f5c4b0b66ae..98d7e799d67 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -4,7 +4,10 @@ require 'spec_helper'
describe Clusters::Cluster do
it { is_expected.to belong_to(:user) }
+ it { is_expected.to have_many(:cluster_projects) }
it { is_expected.to have_many(:projects) }
+ it { is_expected.to have_many(:cluster_groups) }
+ it { is_expected.to have_many(:groups) }
it { is_expected.to have_one(:provider_gcp) }
it { is_expected.to have_one(:platform_kubernetes) }
it { is_expected.to have_one(:application_helm) }
@@ -13,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) }
@@ -178,6 +182,53 @@ describe Clusters::Cluster do
it { expect(cluster.update(enabled: false)).to be_truthy }
end
end
+
+ describe 'cluster_type validations' do
+ let(:instance_cluster) { create(:cluster, :instance) }
+ let(:group_cluster) { create(:cluster, :group) }
+ let(:project_cluster) { create(:cluster, :project) }
+
+ it 'validates presence' do
+ cluster = build(:cluster, :project, cluster_type: nil)
+
+ expect(cluster).not_to be_valid
+ expect(cluster.errors.full_messages).to include("Cluster type can't be blank")
+ end
+
+ context 'project_type cluster' do
+ it 'does not allow setting group' do
+ project_cluster.groups << build(:group)
+
+ expect(project_cluster).not_to be_valid
+ expect(project_cluster.errors.full_messages).to include('Cluster cannot have groups assigned')
+ end
+ end
+
+ context 'group_type cluster' do
+ it 'does not allow setting project' do
+ group_cluster.projects << build(:project)
+
+ expect(group_cluster).not_to be_valid
+ expect(group_cluster.errors.full_messages).to include('Cluster cannot have projects assigned')
+ end
+ end
+
+ context 'instance_type cluster' do
+ it 'does not allow setting group' do
+ instance_cluster.groups << build(:group)
+
+ expect(instance_cluster).not_to be_valid
+ expect(instance_cluster.errors.full_messages).to include('Cluster cannot have groups assigned')
+ end
+
+ it 'does not allow setting project' do
+ instance_cluster.projects << build(:project)
+
+ expect(instance_cluster).not_to be_valid
+ expect(instance_cluster.errors.full_messages).to include('Cluster cannot have projects assigned')
+ end
+ end
+ end
end
describe '#provider' do
@@ -229,6 +280,23 @@ describe Clusters::Cluster do
end
end
+ describe '#group' do
+ subject { cluster.group }
+
+ context 'when cluster belongs to a group' do
+ let(:cluster) { create(:cluster, :group) }
+ let(:group) { cluster.groups.first }
+
+ it { is_expected.to eq(group) }
+ end
+
+ context 'when cluster does not belong to any group' do
+ let(:cluster) { create(:cluster) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
describe '#applications' do
set(:cluster) { create(:cluster) }
@@ -246,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
@@ -274,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/group_spec.rb b/spec/models/clusters/group_spec.rb
new file mode 100644
index 00000000000..ba145342cb8
--- /dev/null
+++ b/spec/models/clusters/group_spec.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::Group do
+ it { is_expected.to belong_to(:cluster) }
+ it { is_expected.to belong_to(:group) }
+end
diff --git a/spec/models/clusters/kubernetes_namespace_spec.rb b/spec/models/clusters/kubernetes_namespace_spec.rb
index dea58fa26c7..56c98d016c9 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 }
+
+ 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) }
- let(:kubernetes_namespace) do
- build(:cluster_kubernetes_namespace,
- cluster: cluster_project.cluster,
- project: cluster_project.project,
- cluster_project: cluster_project)
+ 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 '#set_defaults' 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.set_defaults }
- 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 }
+ describe '#service_account_name' do
+ 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/commit_spec.rb b/spec/models/commit_spec.rb
index ed41ff7a0fa..2a0039a0635 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -72,6 +72,7 @@ describe Commit do
context 'using eager loading' do
let!(:alice) { create(:user, email: 'alice@example.com') }
let!(:bob) { create(:user, email: 'hunter2@example.com') }
+ let!(:jeff) { create(:user) }
let(:alice_commit) do
described_class.new(RepoHelpers.sample_commit, project).tap do |c|
@@ -93,7 +94,14 @@ describe Commit do
end
end
- let!(:commits) { [alice_commit, bob_commit, eve_commit] }
+ let(:jeff_commit) do
+ # The commit for Jeff uses his private commit email
+ described_class.new(RepoHelpers.sample_commit, project).tap do |c|
+ c.author_email = jeff.private_commit_email
+ end
+ end
+
+ let!(:commits) { [alice_commit, bob_commit, eve_commit, jeff_commit] }
before do
create(:email, user: bob, email: 'bob@example.com')
@@ -125,6 +133,20 @@ describe Commit do
expect(bob_commit.author).to eq(bob)
end
+ it "preloads the authors for Commits using a User's private commit Email" do
+ commits.each(&:lazy_author)
+
+ expect(jeff_commit.author).to eq(jeff)
+ end
+
+ it "preloads the authors for Commits using a User's outdated private commit Email" do
+ jeff.update!(username: 'new-username')
+
+ commits.each(&:lazy_author)
+
+ expect(jeff_commit.author).to eq(jeff)
+ end
+
it 'sets the author to Nil if an author could not be found for a Commit' do
commits.each(&:lazy_author)
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/concerns/redactable_spec.rb b/spec/models/concerns/redactable_spec.rb
new file mode 100644
index 00000000000..7d320edd492
--- /dev/null
+++ b/spec/models/concerns/redactable_spec.rb
@@ -0,0 +1,69 @@
+require 'spec_helper'
+
+describe Redactable do
+ shared_examples 'model with redactable field' do
+ it 'redacts unsubscribe token' do
+ model[field] = 'some text /sent_notifications/00000000000000000000000000000000/unsubscribe more text'
+
+ model.save!
+
+ expect(model[field]).to eq 'some text /sent_notifications/REDACTED/unsubscribe more text'
+ end
+
+ it 'ignores not hexadecimal tokens' do
+ text = 'some text /sent_notifications/token/unsubscribe more text'
+ model[field] = text
+
+ model.save!
+
+ expect(model[field]).to eq text
+ end
+
+ it 'ignores not matching texts' do
+ text = 'some text /sent_notifications/.*/unsubscribe more text'
+ model[field] = text
+
+ model.save!
+
+ expect(model[field]).to eq text
+ end
+
+ it 'redacts the field when saving the model before creating markdown cache' do
+ model[field] = 'some text /sent_notifications/00000000000000000000000000000000/unsubscribe more text'
+
+ model.save!
+
+ expected = 'some text /sent_notifications/REDACTED/unsubscribe more text'
+ expect(model[field]).to eq expected
+ expect(model["#{field}_html"]).to eq "<p dir=\"auto\">#{expected}</p>"
+ end
+ end
+
+ context 'when model is an issue' do
+ it_behaves_like 'model with redactable field' do
+ let(:model) { create(:issue) }
+ let(:field) { :description }
+ end
+ end
+
+ context 'when model is a merge request' do
+ it_behaves_like 'model with redactable field' do
+ let(:model) { create(:merge_request) }
+ let(:field) { :description }
+ end
+ end
+
+ context 'when model is a note' do
+ it_behaves_like 'model with redactable field' do
+ let(:model) { create(:note) }
+ let(:field) { :note }
+ end
+ end
+
+ context 'when model is a snippet' do
+ it_behaves_like 'model with redactable field' do
+ let(:model) { create(:snippet) }
+ let(:field) { :description }
+ end
+ end
+end
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index 9b804429138..782687516ae 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -2,8 +2,6 @@ require 'spec_helper'
shared_examples 'TokenAuthenticatable' do
describe 'dynamically defined methods' do
- it { expect(described_class).to be_private_method_defined(:generate_token) }
- it { expect(described_class).to be_private_method_defined(:write_new_token) }
it { expect(described_class).to respond_to("find_by_#{token_field}") }
it { is_expected.to respond_to("ensure_#{token_field}") }
it { is_expected.to respond_to("set_#{token_field}") }
@@ -66,13 +64,275 @@ describe ApplicationSetting, 'TokenAuthenticatable' do
end
describe 'multiple token fields' do
- before do
+ before(:all) do
described_class.send(:add_authentication_token_field, :yet_another_token)
end
- describe '.token_fields' do
- subject { described_class.authentication_token_fields }
- it { is_expected.to include(:runners_registration_token, :yet_another_token) }
+ it { is_expected.to respond_to(:ensure_runners_registration_token) }
+ it { is_expected.to respond_to(:ensure_yet_another_token) }
+ end
+
+ describe 'setting same token field multiple times' do
+ subject { described_class.send(:add_authentication_token_field, :runners_registration_token) }
+
+ it 'raises error' do
+ expect {subject}.to raise_error(ArgumentError)
+ end
+ end
+end
+
+describe PersonalAccessToken, 'TokenAuthenticatable' do
+ let(:personal_access_token_name) { 'test-pat-01' }
+ let(:token_value) { 'token' }
+ let(:user) { create(:user) }
+ let(:personal_access_token) do
+ described_class.new(name: personal_access_token_name,
+ user_id: user.id,
+ scopes: [:api],
+ token: token,
+ token_digest: token_digest)
+ end
+
+ before do
+ allow(Devise).to receive(:friendly_token).and_return(token_value)
+ end
+
+ describe '.find_by_token' do
+ subject { PersonalAccessToken.find_by_token(token_value) }
+
+ before do
+ personal_access_token.save
+ end
+
+ context 'token_digest already exists' do
+ let(:token) { nil }
+ let(:token_digest) { Gitlab::CryptoHelper.sha256(token_value) }
+
+ it 'finds the token' do
+ expect(subject).not_to be_nil
+ expect(subject.name).to eql(personal_access_token_name)
+ end
+ end
+
+ context 'token_digest does not exist' do
+ let(:token) { token_value }
+ let(:token_digest) { nil }
+
+ it 'finds the token' do
+ expect(subject).not_to be_nil
+ expect(subject.name).to eql(personal_access_token_name)
+ end
+ end
+ end
+
+ describe '#set_token' do
+ let(:new_token_value) { 'new-token' }
+ subject { personal_access_token.set_token(new_token_value) }
+
+ context 'token_digest already exists' do
+ let(:token) { nil }
+ let(:token_digest) { Gitlab::CryptoHelper.sha256(token_value) }
+
+ it 'overwrites token_digest' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to eql(new_token_value)
+ expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(new_token_value))
+ end
+ end
+
+ context 'token_digest does not exist but token does' do
+ let(:token) { token_value }
+ let(:token_digest) { nil }
+
+ it 'creates new token_digest and clears token' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to eql(new_token_value)
+ expect(personal_access_token.token_digest).to eql(Gitlab::CryptoHelper.sha256(new_token_value))
+ end
+ end
+
+ context 'token_digest does not exist, nor token' do
+ let(:token) { nil }
+ let(:token_digest) { nil }
+
+ it 'creates new token_digest' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to eql(new_token_value)
+ expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(new_token_value))
+ end
+ end
+ end
+
+ describe '#ensure_token' do
+ subject { personal_access_token.ensure_token }
+
+ context 'token_digest already exists' do
+ let(:token) { nil }
+ let(:token_digest) { Gitlab::CryptoHelper.sha256(token_value) }
+
+ it 'does not change token fields' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to be_nil
+ expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
+ end
+ end
+
+ context 'token_digest does not exist but token does' do
+ let(:token) { token_value }
+ let(:token_digest) { nil }
+
+ it 'does not change token fields' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to eql(token_value)
+ expect(personal_access_token.token).to eql(token_value)
+ expect(personal_access_token.token_digest).to be_nil
+ end
+ end
+
+ context 'token_digest does not exist, nor token' do
+ let(:token) { nil }
+ let(:token_digest) { nil }
+
+ it 'creates token_digest' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to eql(token_value)
+ expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
+ end
+ end
+ end
+
+ describe '#ensure_token!' do
+ subject { personal_access_token.ensure_token! }
+
+ context 'token_digest already exists' do
+ let(:token) { nil }
+ let(:token_digest) { Gitlab::CryptoHelper.sha256(token_value) }
+
+ it 'does not change token fields' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to be_nil
+ expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
+ end
+ end
+
+ context 'token_digest does not exist but token does' do
+ let(:token) { token_value }
+ let(:token_digest) { nil }
+
+ it 'does not change token fields' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to eql(token_value)
+ expect(personal_access_token.token).to eql(token_value)
+ expect(personal_access_token.token_digest).to be_nil
+ end
+ end
+
+ context 'token_digest does not exist, nor token' do
+ let(:token) { nil }
+ let(:token_digest) { nil }
+
+ it 'creates token_digest' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to eql(token_value)
+ expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
+ end
+ end
+ end
+
+ describe '#reset_token!' do
+ subject { personal_access_token.reset_token! }
+
+ context 'token_digest already exists' do
+ let(:token) { nil }
+ let(:token_digest) { Gitlab::CryptoHelper.sha256('old-token') }
+
+ it 'creates new token_digest' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to eql(token_value)
+ expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
+ end
+ end
+
+ context 'token_digest does not exist but token does' do
+ let(:token) { 'old-token' }
+ let(:token_digest) { nil }
+
+ it 'creates new token_digest and clears token' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to eql(token_value)
+ expect(personal_access_token.token_digest).to eql(Gitlab::CryptoHelper.sha256(token_value))
+ end
+ end
+
+ context 'token_digest does not exist, nor token' do
+ let(:token) { nil }
+ let(:token_digest) { nil }
+
+ it 'creates new token_digest' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to eql(token_value)
+ expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
+ end
+ end
+
+ context 'token_digest exists and newly generated token would be the same' do
+ let(:token) { nil }
+ let(:token_digest) { Gitlab::CryptoHelper.sha256('old-token') }
+
+ before do
+ personal_access_token.save
+ allow(Devise).to receive(:friendly_token).and_return(
+ 'old-token', token_value, 'boom!')
+ end
+
+ it 'regenerates a new token_digest' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to eql(token_value)
+ expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
+ end
+ end
+
+ context 'token exists and newly generated token would be the same' do
+ let(:token) { 'old-token' }
+ let(:token_digest) { nil }
+
+ before do
+ personal_access_token.save
+ allow(Devise).to receive(:friendly_token).and_return(
+ 'old-token', token_value, 'boom!')
+ end
+
+ it 'regenerates a new token_digest' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to eql(token_value)
+ expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
+ end
end
end
end
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 c65e0b81451..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,14 +352,78 @@ 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? }
- context 'when the enviroment is available' do
+ context 'when the environment is available' 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
@@ -447,7 +532,7 @@ describe Environment do
describe '#has_metrics?' do
subject { environment.has_metrics? }
- context 'when the enviroment is available' do
+ context 'when the environment is available' do
context 'with a deployment service' do
let(:project) { create(:prometheus_project) }
@@ -456,8 +541,8 @@ describe Environment do
it { is_expected.to be_truthy }
end
- context 'but no deployments' do
- it { is_expected.to be_falsy }
+ context 'and no deployments' do
+ it { is_expected.to be_truthy }
end
end
@@ -504,39 +589,6 @@ describe Environment do
end
end
- describe '#has_metrics?' do
- subject { environment.has_metrics? }
-
- context 'when the enviroment is available' do
- context 'with a deployment service' do
- let(:project) { create(:prometheus_project) }
-
- context 'and a deployment' do
- let!(:deployment) { create(:deployment, environment: environment) }
- it { is_expected.to be_truthy }
- end
-
- context 'but no deployments' do
- it { is_expected.to be_falsy }
- end
- end
-
- context 'without a monitoring service' do
- it { is_expected.to be_falsy }
- end
- end
-
- context 'when the environment is unavailable' do
- let(:project) { create(:prometheus_project) }
-
- before do
- environment.stop
- end
-
- it { is_expected.to be_falsy }
- end
- end
-
describe '#additional_metrics' do
let(:project) { create(:prometheus_project) }
subject { environment.additional_metrics }
diff --git a/spec/models/environment_status_spec.rb b/spec/models/environment_status_spec.rb
index f2eb263c98c..90f7e4a4590 100644
--- a/spec/models/environment_status_spec.rb
+++ b/spec/models/environment_status_spec.rb
@@ -1,17 +1,21 @@
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 }
- subject(:environment_status) { described_class.new(environment, merge_request) }
+ subject(:environment_status) { described_class.new(environment, merge_request, sha) }
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
subject { environment_status.project }
@@ -58,4 +62,104 @@ describe EnvironmentStatus do
)
end
end
+
+ describe '.for_merge_request' do
+ let(:admin) { create(:admin) }
+ let(:pipeline) { create(:ci_pipeline, sha: sha) }
+
+ 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
+ end
+
+ describe '.after_merge_request' do
+ let(:admin) { create(:admin) }
+ let(:pipeline) { create(:ci_pipeline, sha: sha) }
+
+ before do
+ merge_request.mark_as_merged!
+ end
+
+ 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/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb
index ab58f5c5021..b6355455c1d 100644
--- a/spec/models/global_milestone_spec.rb
+++ b/spec/models/global_milestone_spec.rb
@@ -92,41 +92,6 @@ describe GlobalMilestone do
end
end
- describe '.states_count' do
- context 'when the projects have milestones' do
- before do
- create(:closed_milestone, title: 'Active Group Milestone', project: project3)
- create(:active_milestone, title: 'Active Group Milestone', project: project1)
- create(:active_milestone, title: 'Active Group Milestone', project: project2)
- create(:closed_milestone, title: 'Closed Group Milestone', project: project1)
- create(:closed_milestone, title: 'Closed Group Milestone', project: project2)
- create(:closed_milestone, title: 'Closed Group Milestone', project: project3)
- end
-
- it 'returns the quantity of global milestones in each possible state' do
- expected_count = { opened: 1, closed: 2, all: 2 }
-
- count = described_class.states_count(Project.all)
-
- expect(count).to eq(expected_count)
- end
- end
-
- context 'when the projects do not have milestones' do
- before do
- project1
- end
-
- it 'returns 0 as the quantity of global milestones in each state' do
- expected_count = { opened: 0, closed: 0, all: 0 }
-
- count = described_class.states_count(Project.all)
-
- expect(count).to eq(expected_count)
- end
- end
- end
-
describe '#initialize' do
let(:milestone1_project1) { create(:milestone, title: "Milestone v1.2", project: project1) }
let(:milestone1_project2) { create(:milestone, title: "Milestone v1.2", project: project2) }
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 1bf8f89e126..ada00f03928 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -19,6 +19,8 @@ describe Group do
it { is_expected.to have_one(:chat_team) }
it { is_expected.to have_many(:custom_attributes).class_name('GroupCustomAttribute') }
it { is_expected.to have_many(:badges).class_name('GroupBadge') }
+ it { is_expected.to have_many(:cluster_groups).class_name('Clusters::Group') }
+ it { is_expected.to have_many(:clusters).class_name('Clusters::Cluster') }
describe '#members & #requesters' do
let(:requester) { create(:user) }
@@ -651,10 +653,10 @@ describe Group do
end
end
- describe '#secret_variables_for' do
+ describe '#ci_variables_for' do
let(:project) { create(:project, group: group) }
- let!(:secret_variable) do
+ let!(:ci_variable) do
create(:ci_group_variable, value: 'secret', group: group)
end
@@ -662,11 +664,11 @@ describe Group do
create(:ci_group_variable, :protected, value: 'protected', group: group)
end
- subject { group.secret_variables_for('ref', project) }
+ subject { group.ci_variables_for('ref', project) }
shared_examples 'ref is protected' do
it 'contains all the variables' do
- is_expected.to contain_exactly(secret_variable, protected_variable)
+ is_expected.to contain_exactly(ci_variable, protected_variable)
end
end
@@ -676,8 +678,8 @@ describe Group do
default_branch_protection: Gitlab::Access::PROTECTION_NONE)
end
- it 'contains only the secret variables' do
- is_expected.to contain_exactly(secret_variable)
+ it 'contains only the CI variables' do
+ is_expected.to contain_exactly(ci_variable)
end
end
@@ -710,9 +712,9 @@ describe Group do
end
it 'returns all variables belong to the group and parent groups' do
- expected_array1 = [protected_variable, secret_variable]
+ expected_array1 = [protected_variable, ci_variable]
expected_array2 = [variable_child, variable_child_2, variable_child_3]
- got_array = group_child_3.secret_variables_for('ref', project).to_a
+ got_array = group_child_3.ci_variables_for('ref', project).to_a
expect(got_array.shift(2)).to contain_exactly(*expected_array1)
expect(got_array).to eq(expected_array2)
diff --git a/spec/models/lfs_object_spec.rb b/spec/models/lfs_object_spec.rb
index 6e35511e848..3f929710862 100644
--- a/spec/models/lfs_object_spec.rb
+++ b/spec/models/lfs_object_spec.rb
@@ -2,19 +2,13 @@ require 'spec_helper'
describe LfsObject do
describe '#local_store?' do
- it 'returns true when file_store is nil' do
- subject.file_store = nil
-
- expect(subject.local_store?).to eq true
- end
-
it 'returns true when file_store is equal to LfsObjectUploader::Store::LOCAL' do
subject.file_store = LfsObjectUploader::Store::LOCAL
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
@@ -83,19 +77,6 @@ describe LfsObject do
describe 'file is being stored' do
let(:lfs_object) { create(:lfs_object, :with_file) }
- context 'when object has nil store' do
- before do
- lfs_object.update_column(:file_store, nil)
- lfs_object.reload
- end
-
- it 'is stored locally' do
- expect(lfs_object.file_store).to be(nil)
- expect(lfs_object.file).to be_file_storage
- expect(lfs_object.file.object_store).to eq(ObjectStorage::Store::LOCAL)
- end
- end
-
context 'when existing object has local store' do
it 'is stored locally' do
expect(lfs_object.file_store).to be(ObjectStorage::Store::LOCAL)
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 666d7e69f89..ad55c280399 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -13,51 +13,55 @@ 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) }
- end
- describe '#squash_in_progress?' do
- shared_examples 'checking whether a squash is in progress' do
- let(:repo_path) do
- Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- subject.source_project.repository.path
- end
- end
- let(:squash_path) { File.join(repo_path, "gitlab-worktree", "squash-#{subject.id}") }
+ 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) }
- before do
- system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} worktree add --detach #{squash_path} master))
+ 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 'returns true when there is a current squash directory' do
- expect(subject.squash_in_progress?).to be_truthy
+ it 'finds the associated merge request' do
+ expect(project.merge_requests.find(merge_request.id)).to eq(merge_request)
end
+ end
+ end
- it 'returns false when there is no squash directory' do
- FileUtils.rm_rf(squash_path)
-
- expect(subject.squash_in_progress?).to be_falsey
+ describe '#squash_in_progress?' do
+ let(:repo_path) do
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ subject.source_project.repository.path
end
+ end
+ let(:squash_path) { File.join(repo_path, "gitlab-worktree", "squash-#{subject.id}") }
- it 'returns false when the squash directory has expired' do
- time = 20.minutes.ago.to_time
- File.utime(time, time, squash_path)
+ before do
+ system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} worktree add --detach #{squash_path} master))
+ end
- expect(subject.squash_in_progress?).to be_falsey
- end
+ it 'returns true when there is a current squash directory' do
+ expect(subject.squash_in_progress?).to be_truthy
+ end
- it 'returns false when the source project has been removed' do
- allow(subject).to receive(:source_project).and_return(nil)
+ it 'returns false when there is no squash directory' do
+ FileUtils.rm_rf(squash_path)
- expect(subject.squash_in_progress?).to be_falsey
- end
+ expect(subject.squash_in_progress?).to be_falsey
end
- context 'when Gitaly squash_in_progress is enabled' do
- it_behaves_like 'checking whether a squash is in progress'
+ it 'returns false when the squash directory has expired' do
+ time = 20.minutes.ago.to_time
+ File.utime(time, time, squash_path)
+
+ expect(subject.squash_in_progress?).to be_falsey
end
- context 'when Gitaly squash_in_progress is disabled', :disable_gitaly do
- it_behaves_like 'checking whether a squash is in progress'
+ it 'returns false when the source project has been removed' do
+ allow(subject).to receive(:source_project).and_return(nil)
+
+ expect(subject.squash_in_progress?).to be_falsey
end
end
@@ -538,9 +542,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 +621,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) }
@@ -1058,6 +1100,26 @@ describe MergeRequest do
end
end
+ describe '#merge_pipeline' do
+ it 'returns nil when not merged' do
+ expect(subject.merge_pipeline).to be_nil
+ end
+
+ context 'when the MR is merged' do
+ let(:sha) { subject.target_project.commit.id }
+ let(:pipeline) { create(:ci_empty_pipeline, sha: sha, ref: subject.target_branch, project: subject.target_project) }
+
+ before do
+ subject.mark_as_merged!
+ subject.update_attribute(:merge_commit_sha, pipeline.sha)
+ end
+
+ it 'returns the post-merge pipeline' do
+ expect(subject.merge_pipeline).to eq(pipeline)
+ end
+ end
+ end
+
describe '#has_ci?' do
let(:merge_request) { build_stubbed(:merge_request) }
@@ -1710,7 +1772,7 @@ describe MergeRequest do
allow(subject).to receive(:head_pipeline) { nil }
end
- it { expect(subject.mergeable_ci_state?).to be_truthy }
+ it { expect(subject.mergeable_ci_state?).to be_falsey }
end
end
@@ -1802,8 +1864,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
@@ -1823,7 +1885,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
@@ -1834,7 +1896,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
@@ -2515,14 +2577,6 @@ describe MergeRequest do
expect(subject.rebase_in_progress?).to be_falsey
end
end
-
- context 'when Gitaly rebase_in_progress is enabled' do
- it_behaves_like 'checking whether a rebase is in progress'
- end
-
- context 'when Gitaly rebase_in_progress is enabled', :disable_gitaly do
- it_behaves_like 'checking whether a rebase is in progress'
- end
end
describe '#allow_collaboration' do
@@ -2577,6 +2631,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/milestone_spec.rb b/spec/models/milestone_spec.rb
index 27d4e622710..d11eb46159e 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -348,4 +348,41 @@ describe Milestone do
end
end
end
+
+ describe '.states_count' do
+ context 'when the projects have milestones' do
+ before do
+ project_1 = create(:project)
+ project_2 = create(:project)
+ group_1 = create(:group)
+ group_2 = create(:group)
+
+ create(:active_milestone, title: 'Active Group Milestone', project: project_1)
+ create(:closed_milestone, title: 'Closed Group Milestone', project: project_1)
+ create(:active_milestone, title: 'Active Group Milestone', project: project_2)
+ create(:closed_milestone, title: 'Closed Group Milestone', project: project_2)
+ create(:closed_milestone, title: 'Active Group Milestone', group: group_1)
+ create(:closed_milestone, title: 'Closed Group Milestone', group: group_1)
+ create(:closed_milestone, title: 'Active Group Milestone', group: group_2)
+ create(:closed_milestone, title: 'Closed Group Milestone', group: group_2)
+ end
+
+ it 'returns the quantity of milestones in each possible state' do
+ expected_count = { opened: 5, closed: 6, all: 11 }
+
+ count = described_class.states_count(Project.all, Group.all)
+ expect(count).to eq(expected_count)
+ end
+ end
+
+ context 'when the projects do not have milestones' do
+ it 'returns 0 as the quantity of global milestones in each state' do
+ expected_count = { opened: 0, closed: 0, all: 0 }
+
+ count = described_class.states_count([project])
+
+ expect(count).to eq(expected_count)
+ end
+ end
+ end
end
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/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
index 2bb1c49b740..c82ab9c9e62 100644
--- a/spec/models/personal_access_token_spec.rb
+++ b/spec/models/personal_access_token_spec.rb
@@ -49,18 +49,36 @@ describe PersonalAccessToken do
describe 'Redis storage' do
let(:user_id) { 123 }
- let(:token) { 'abc000foo' }
+ let(:token) { 'KS3wegQYXBLYhQsciwsj' }
- before do
- subject.redis_store!(user_id, token)
+ context 'reading encrypted data' do
+ before do
+ subject.redis_store!(user_id, token)
+ end
+
+ it 'returns stored data' do
+ expect(subject.redis_getdel(user_id)).to eq(token)
+ end
end
- it 'returns stored data' do
- expect(subject.redis_getdel(user_id)).to eq(token)
+ context 'reading unencrypted data' do
+ before do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set(described_class.redis_shared_state_key(user_id),
+ token,
+ ex: PersonalAccessToken::REDIS_EXPIRY_TIME)
+ end
+ end
+
+ it 'returns stored data unmodified' do
+ expect(subject.redis_getdel(user_id)).to eq(token)
+ end
end
context 'after deletion' do
before do
+ subject.redis_store!(user_id, token)
+
expect(subject.redis_getdel(user_id)).to eq(token)
end
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/chat_message/push_message_spec.rb b/spec/models/project_services/chat_message/push_message_spec.rb
index 19c2862264f..973d6bdb2a0 100644
--- a/spec/models/project_services/chat_message/push_message_spec.rb
+++ b/spec/models/project_services/chat_message/push_message_spec.rb
@@ -48,12 +48,12 @@ describe ChatMessage::PushMessage do
'test.user pushed to branch [master](http://url.com/commits/master) of [project_name](http://url.com) ([Compare changes](http://url.com/compare/before...after))')
expect(subject.attachments).to eq(
"[abcdefgh](http://url1.com): message1 - author1\n\n[12345678](http://url2.com): message2 - author2")
- expect(subject.activity).to eq({
- title: 'test.user pushed to branch',
+ expect(subject.activity).to eq(
+ title: 'test.user pushed to branch [master](http://url.com/commits/master)',
subtitle: 'in [project_name](http://url.com)',
text: '[Compare changes](http://url.com/compare/before...after)',
image: 'http://someavatar.com'
- })
+ )
end
end
end
@@ -89,12 +89,53 @@ describe ChatMessage::PushMessage do
expect(subject.pretext).to eq(
'test.user pushed new tag [new_tag](http://url.com/commits/new_tag) to [project_name](http://url.com)')
expect(subject.attachments).to be_empty
- expect(subject.activity).to eq({
- title: 'test.user created tag',
+ expect(subject.activity).to eq(
+ title: 'test.user pushed new tag [new_tag](http://url.com/commits/new_tag)',
subtitle: 'in [project_name](http://url.com)',
text: '[Compare changes](http://url.com/compare/0000000000000000000000000000000000000000...after)',
image: 'http://someavatar.com'
- })
+ )
+ end
+ end
+ end
+
+ context 'removed tag' do
+ let(:args) do
+ {
+ after: Gitlab::Git::BLANK_SHA,
+ before: 'before',
+ project_name: 'project_name',
+ ref: 'refs/tags/new_tag',
+ user_name: 'test.user',
+ user_avatar: 'http://someavatar.com',
+ project_url: 'http://url.com'
+ }
+ end
+
+ context 'without markdown' do
+ it 'returns a message regarding removal of tags' do
+ expect(subject.pretext).to eq('test.user removed tag ' \
+ 'new_tag from ' \
+ '<http://url.com|project_name>')
+ expect(subject.attachments).to be_empty
+ end
+ end
+
+ context 'with markdown' do
+ before do
+ args[:markdown] = true
+ end
+
+ it 'returns a message regarding removal of tags' do
+ expect(subject.pretext).to eq(
+ 'test.user removed tag new_tag from [project_name](http://url.com)')
+ expect(subject.attachments).to be_empty
+ expect(subject.activity).to eq(
+ title: 'test.user removed tag new_tag',
+ subtitle: 'in [project_name](http://url.com)',
+ text: '[Compare changes](http://url.com/compare/before...0000000000000000000000000000000000000000)',
+ image: 'http://someavatar.com'
+ )
end
end
end
@@ -122,12 +163,12 @@ describe ChatMessage::PushMessage do
expect(subject.pretext).to eq(
'test.user pushed new branch [master](http://url.com/commits/master) to [project_name](http://url.com)')
expect(subject.attachments).to be_empty
- expect(subject.activity).to eq({
- title: 'test.user created branch',
+ expect(subject.activity).to eq(
+ title: 'test.user pushed new branch [master](http://url.com/commits/master)',
subtitle: 'in [project_name](http://url.com)',
text: '[Compare changes](http://url.com/compare/0000000000000000000000000000000000000000...after)',
image: 'http://someavatar.com'
- })
+ )
end
end
end
@@ -154,12 +195,12 @@ describe ChatMessage::PushMessage do
expect(subject.pretext).to eq(
'test.user removed branch master from [project_name](http://url.com)')
expect(subject.attachments).to be_empty
- expect(subject.activity).to eq({
- title: 'test.user removed branch',
+ expect(subject.activity).to eq(
+ title: 'test.user removed branch master',
subtitle: 'in [project_name](http://url.com)',
text: '[Compare changes](http://url.com/compare/before...0000000000000000000000000000000000000000)',
image: 'http://someavatar.com'
- })
+ )
end
end
end
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/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index 0cd712e2f40..b0fd2ceead0 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -387,4 +387,22 @@ describe HipchatService do
end
end
end
+
+ context 'with UrlBlocker' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:hipchat) { described_class.new(project: project) }
+ let(:push_sample_data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
+
+ describe '#execute' do
+ before do
+ hipchat.server = 'http://localhost:9123'
+ end
+
+ it 'raises UrlBlocker for localhost' do
+ expect(Gitlab::UrlBlocker).to receive(:validate!).and_call_original
+ expect { hipchat.execute(push_sample_data) }.to raise_error(Gitlab::HTTP::BlockedUrlError)
+ end
+ end
+ 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 62a38c66d99..51278836604 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
@@ -2432,10 +2450,10 @@ describe Project do
end
end
- describe '#secret_variables_for' do
+ describe '#ci_variables_for' do
let(:project) { create(:project) }
- let!(:secret_variable) do
+ let!(:ci_variable) do
create(:ci_variable, value: 'secret', project: project)
end
@@ -2443,7 +2461,7 @@ describe Project do
create(:ci_variable, :protected, value: 'protected', project: project)
end
- subject { project.reload.secret_variables_for(ref: 'ref') }
+ subject { project.reload.ci_variables_for(ref: 'ref') }
before do
stub_application_setting(
@@ -2452,13 +2470,13 @@ describe Project do
shared_examples 'ref is protected' do
it 'contains all the variables' do
- is_expected.to contain_exactly(secret_variable, protected_variable)
+ is_expected.to contain_exactly(ci_variable, protected_variable)
end
end
context 'when the ref is not protected' do
- it 'contains only the secret variables' do
- is_expected.to contain_exactly(secret_variable)
+ it 'contains only the CI variables' do
+ is_expected.to contain_exactly(ci_variable)
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]) }
@@ -3069,6 +3087,14 @@ describe Project do
it 'does not flag as read-only' do
expect { project.migrate_to_hashed_storage! }.not_to change { project.repository_read_only }
end
+
+ context 'when partially migrated' do
+ it 'returns true' do
+ project = create(:project, storage_version: 1, skip_disk_validation: true)
+
+ expect(project.migrate_to_hashed_storage!).to be_truthy
+ end
+ end
end
end
@@ -3314,7 +3340,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 +3985,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..48a43801b9f 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -130,63 +130,53 @@ describe ProjectWiki do
end
describe "#find_page" do
- shared_examples 'finding a wiki page' do
- before do
- create_page("index page", "This is an awesome Gollum Wiki")
- end
-
- after do
- subject.pages.each { |page| destroy_page(page.page) }
- end
+ before do
+ create_page("index page", "This is an awesome Gollum Wiki")
+ end
- it "returns the latest version of the page if it exists" do
- page = subject.find_page("index page")
- expect(page.title).to eq("index page")
- end
+ after do
+ subject.pages.each { |page| destroy_page(page.page) }
+ end
- it "returns nil if the page does not exist" do
- expect(subject.find_page("non-existant")).to eq(nil)
- end
+ it "returns the latest version of the page if it exists" do
+ page = subject.find_page("index page")
+ expect(page.title).to eq("index page")
+ end
- it "can find a page by slug" do
- page = subject.find_page("index-page")
- expect(page.title).to eq("index page")
- end
+ it "returns nil if the page does not exist" do
+ expect(subject.find_page("non-existent")).to eq(nil)
+ end
- it "returns a WikiPage instance" do
- page = subject.find_page("index page")
- expect(page).to be_a WikiPage
- end
+ it "can find a page by slug" do
+ page = subject.find_page("index-page")
+ expect(page.title).to eq("index page")
+ end
- context 'pages with multibyte-character title' do
- before do
- create_page("autre pagé", "C'est un génial Gollum Wiki")
- end
+ it "returns a WikiPage instance" do
+ page = subject.find_page("index page")
+ expect(page).to be_a WikiPage
+ end
- it "can find a page by slug" do
- page = subject.find_page("autre pagé")
- expect(page.title).to eq("autre pagé")
- end
+ context 'pages with multibyte-character title' do
+ before do
+ create_page("autre pagé", "C'est un génial Gollum Wiki")
end
- context 'pages with invalidly-encoded content' do
- before do
- create_page("encoding is fun", "f\xFCr".b)
- end
-
- it "can find the page" do
- page = subject.find_page("encoding is fun")
- expect(page.content).to eq("fr")
- end
+ it "can find a page by slug" do
+ page = subject.find_page("autre pagé")
+ expect(page.title).to eq("autre pagé")
end
end
- context 'when Gitaly wiki_find_page is enabled' do
- it_behaves_like 'finding a wiki page'
- end
+ context 'pages with invalidly-encoded content' do
+ before do
+ create_page("encoding is fun", "f\xFCr".b)
+ end
- context 'when Gitaly wiki_find_page is disabled', :skip_gitaly_mock do
- it_behaves_like 'finding a wiki page'
+ it "can find the page" do
+ page = subject.find_page("encoding is fun")
+ expect(page.content).to eq("fr")
+ end
end
end
@@ -207,100 +197,80 @@ describe ProjectWiki do
end
describe '#find_file' do
- shared_examples 'finding a wiki file' do
- let(:image) { File.open(Rails.root.join('spec', 'fixtures', 'big-image.png')) }
-
- before do
- subject.wiki # Make sure the wiki repo exists
-
- repo_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- subject.repository.path_to_repo
- end
-
- BareRepoOperations.new(repo_path).commit_file(image, 'image.png')
- end
+ let(:image) { File.open(Rails.root.join('spec', 'fixtures', 'big-image.png')) }
- it 'returns the latest version of the file if it exists' do
- file = subject.find_file('image.png')
- expect(file.mime_type).to eq('image/png')
- end
+ before do
+ subject.wiki # Make sure the wiki repo exists
- it 'returns nil if the page does not exist' do
- expect(subject.find_file('non-existant')).to eq(nil)
+ repo_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ subject.repository.path_to_repo
end
- it 'returns a Gitlab::Git::WikiFile instance' do
- file = subject.find_file('image.png')
- expect(file).to be_a Gitlab::Git::WikiFile
- end
+ BareRepoOperations.new(repo_path).commit_file(image, 'image.png')
+ end
- it 'returns the whole file' do
- file = subject.find_file('image.png')
- image.rewind
+ it 'returns the latest version of the file if it exists' do
+ file = subject.find_file('image.png')
+ expect(file.mime_type).to eq('image/png')
+ end
- expect(file.raw_data.b).to eq(image.read.b)
- end
+ it 'returns nil if the page does not exist' do
+ expect(subject.find_file('non-existent')).to eq(nil)
end
- context 'when Gitaly wiki_find_file is enabled' do
- it_behaves_like 'finding a wiki file'
+ it 'returns a Gitlab::Git::WikiFile instance' do
+ file = subject.find_file('image.png')
+ expect(file).to be_a Gitlab::Git::WikiFile
end
- context 'when Gitaly wiki_find_file is disabled', :skip_gitaly_mock do
- it_behaves_like 'finding a wiki file'
+ it 'returns the whole file' do
+ file = subject.find_file('image.png')
+ image.rewind
+
+ expect(file.raw_data.b).to eq(image.read.b)
end
end
describe "#create_page" do
- shared_examples 'creating a wiki page' do
- after do
- destroy_page(subject.pages.first.page)
- end
-
- it "creates a new wiki page" do
- expect(subject.create_page("test page", "this is content")).not_to eq(false)
- expect(subject.pages.count).to eq(1)
- end
-
- it "returns false when a duplicate page exists" do
- subject.create_page("test page", "content")
- expect(subject.create_page("test page", "content")).to eq(false)
- end
-
- it "stores an error message when a duplicate page exists" do
- 2.times { subject.create_page("test page", "content") }
- expect(subject.error_message).to match(/Duplicate page:/)
- end
+ after do
+ destroy_page(subject.pages.first.page)
+ end
- it "sets the correct commit message" do
- subject.create_page("test page", "some content", :markdown, "commit message")
- expect(subject.pages.first.page.version.message).to eq("commit message")
- end
+ it "creates a new wiki page" do
+ expect(subject.create_page("test page", "this is content")).not_to eq(false)
+ expect(subject.pages.count).to eq(1)
+ end
- it 'sets the correct commit email' do
- subject.create_page('test page', 'content')
+ it "returns false when a duplicate page exists" do
+ subject.create_page("test page", "content")
+ expect(subject.create_page("test page", "content")).to eq(false)
+ end
- expect(user.commit_email).not_to eq(user.email)
- expect(commit.author_email).to eq(user.commit_email)
- expect(commit.committer_email).to eq(user.commit_email)
- end
+ it "stores an error message when a duplicate page exists" do
+ 2.times { subject.create_page("test page", "content") }
+ expect(subject.error_message).to match(/Duplicate page:/)
+ end
- it 'updates project activity' do
- subject.create_page('Test Page', 'This is content')
+ it "sets the correct commit message" do
+ subject.create_page("test page", "some content", :markdown, "commit message")
+ expect(subject.pages.first.page.version.message).to eq("commit message")
+ end
- project.reload
+ it 'sets the correct commit email' do
+ subject.create_page('test page', 'content')
- expect(project.last_activity_at).to be_within(1.minute).of(Time.now)
- expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now)
- end
+ expect(user.commit_email).not_to eq(user.email)
+ expect(commit.author_email).to eq(user.commit_email)
+ expect(commit.committer_email).to eq(user.commit_email)
end
- context 'when Gitaly wiki_write_page is enabled' do
- it_behaves_like 'creating a wiki page'
- end
+ it 'updates project activity' do
+ subject.create_page('Test Page', 'This is content')
- context 'when Gitaly wiki_write_page is disabled', :skip_gitaly_mock do
- it_behaves_like 'creating a wiki page'
+ project.reload
+
+ expect(project.last_activity_at).to be_within(1.minute).of(Time.now)
+ expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now)
end
end
@@ -351,41 +321,31 @@ describe ProjectWiki do
end
describe "#delete_page" do
- shared_examples 'deleting a wiki page' do
- before do
- create_page("index", "some content")
- @page = subject.wiki.page(title: "index")
- end
-
- it "deletes the page" do
- subject.delete_page(@page)
- expect(subject.pages.count).to eq(0)
- end
-
- it 'sets the correct commit email' do
- subject.delete_page(@page)
-
- expect(user.commit_email).not_to eq(user.email)
- expect(commit.author_email).to eq(user.commit_email)
- expect(commit.committer_email).to eq(user.commit_email)
- end
+ before do
+ create_page("index", "some content")
+ @page = subject.wiki.page(title: "index")
+ end
- it 'updates project activity' do
- subject.delete_page(@page)
+ it "deletes the page" do
+ subject.delete_page(@page)
+ expect(subject.pages.count).to eq(0)
+ end
- project.reload
+ it 'sets the correct commit email' do
+ subject.delete_page(@page)
- expect(project.last_activity_at).to be_within(1.minute).of(Time.now)
- expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now)
- end
+ expect(user.commit_email).not_to eq(user.email)
+ expect(commit.author_email).to eq(user.commit_email)
+ expect(commit.committer_email).to eq(user.commit_email)
end
- context 'when Gitaly wiki_delete_page is enabled' do
- it_behaves_like 'deleting a wiki page'
- end
+ it 'updates project activity' do
+ subject.delete_page(@page)
+
+ project.reload
- context 'when Gitaly wiki_delete_page is disabled', :skip_gitaly_mock do
- it_behaves_like 'deleting a wiki page'
+ expect(project.last_activity_at).to be_within(1.minute).of(Time.now)
+ expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now)
end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 799a60ac62f..187283b284b 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -38,49 +38,29 @@ describe Repository do
end
describe '#branch_names_contains' do
- shared_examples '#branch_names_contains' do
- set(:project) { create(:project, :repository) }
- let(:repository) { project.repository }
+ set(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
- subject { repository.branch_names_contains(sample_commit.id) }
+ subject { repository.branch_names_contains(sample_commit.id) }
- it { is_expected.to include('master') }
- it { is_expected.not_to include('feature') }
- it { is_expected.not_to include('fix') }
+ it { is_expected.to include('master') }
+ it { is_expected.not_to include('feature') }
+ it { is_expected.not_to include('fix') }
- describe 'when storage is broken', :broken_storage do
- it 'should raise a storage error' do
- expect_to_raise_storage_error do
- broken_repository.branch_names_contains(sample_commit.id)
- end
+ describe 'when storage is broken', :broken_storage do
+ it 'should raise a storage error' do
+ expect_to_raise_storage_error do
+ broken_repository.branch_names_contains(sample_commit.id)
end
end
end
-
- context 'when gitaly is enabled' do
- it_behaves_like '#branch_names_contains'
- end
-
- context 'when gitaly is disabled', :skip_gitaly_mock do
- it_behaves_like '#branch_names_contains'
- end
end
describe '#tag_names_contains' do
- shared_examples '#tag_names_contains' do
- subject { repository.tag_names_contains(sample_commit.id) }
-
- it { is_expected.to include('v1.1.0') }
- it { is_expected.not_to include('v1.0.0') }
- end
-
- context 'when gitaly is enabled' do
- it_behaves_like '#tag_names_contains'
- end
+ subject { repository.tag_names_contains(sample_commit.id) }
- context 'when gitaly is enabled', :skip_gitaly_mock do
- it_behaves_like '#tag_names_contains'
- end
+ it { is_expected.to include('v1.1.0') }
+ it { is_expected.not_to include('v1.0.0') }
end
describe 'tags_sorted_by' do
@@ -238,61 +218,41 @@ describe Repository do
end
describe '#last_commit_for_path' do
- shared_examples 'getting last commit for path' do
- subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id }
+ subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id }
- it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') }
+ it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') }
- describe 'when storage is broken', :broken_storage do
- it 'should raise a storage error' do
- expect_to_raise_storage_error do
- broken_repository.last_commit_id_for_path(sample_commit.id, '.gitignore')
- end
+ describe 'when storage is broken', :broken_storage do
+ it 'should raise a storage error' do
+ expect_to_raise_storage_error do
+ broken_repository.last_commit_id_for_path(sample_commit.id, '.gitignore')
end
end
end
-
- context 'when Gitaly feature last_commit_for_path is enabled' do
- it_behaves_like 'getting last commit for path'
- end
-
- context 'when Gitaly feature last_commit_for_path is disabled', :skip_gitaly_mock do
- it_behaves_like 'getting last commit for path'
- end
end
describe '#last_commit_id_for_path' do
- shared_examples 'getting last commit ID for path' do
- subject { repository.last_commit_id_for_path(sample_commit.id, '.gitignore') }
+ subject { repository.last_commit_id_for_path(sample_commit.id, '.gitignore') }
- it "returns last commit id for a given path" do
- is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8')
- end
+ it "returns last commit id for a given path" do
+ is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8')
+ end
- it "caches last commit id for a given path" do
- cache = repository.send(:cache)
- key = "last_commit_id_for_path:#{sample_commit.id}:#{Digest::SHA1.hexdigest('.gitignore')}"
+ it "caches last commit id for a given path" do
+ cache = repository.send(:cache)
+ key = "last_commit_id_for_path:#{sample_commit.id}:#{Digest::SHA1.hexdigest('.gitignore')}"
- expect(cache).to receive(:fetch).with(key).and_return('c1acaa5')
- is_expected.to eq('c1acaa5')
- end
+ expect(cache).to receive(:fetch).with(key).and_return('c1acaa5')
+ is_expected.to eq('c1acaa5')
+ end
- describe 'when storage is broken', :broken_storage do
- it 'should raise a storage error' do
- expect_to_raise_storage_error do
- broken_repository.last_commit_for_path(sample_commit.id, '.gitignore').id
- end
+ describe 'when storage is broken', :broken_storage do
+ it 'should raise a storage error' do
+ expect_to_raise_storage_error do
+ broken_repository.last_commit_for_path(sample_commit.id, '.gitignore').id
end
end
end
-
- context 'when Gitaly feature last_commit_for_path is enabled' do
- it_behaves_like 'getting last commit ID for path'
- end
-
- context 'when Gitaly feature last_commit_for_path is disabled', :skip_gitaly_mock do
- it_behaves_like 'getting last commit ID for path'
- end
end
describe '#commits' do
@@ -374,78 +334,57 @@ describe Repository do
describe '#commits_by' do
set(:project) { create(:project, :repository) }
+ let(:oids) { TestEnv::BRANCH_SHA.values }
- shared_examples 'batch commits fetching' do
- let(:oids) { TestEnv::BRANCH_SHA.values }
+ subject { project.repository.commits_by(oids: oids) }
- subject { project.repository.commits_by(oids: oids) }
+ it 'finds each commit' do
+ expect(subject).not_to include(nil)
+ expect(subject.size).to eq(oids.size)
+ end
- it 'finds each commit' do
- expect(subject).not_to include(nil)
- expect(subject.size).to eq(oids.size)
- end
+ it 'returns only Commit instances' do
+ expect(subject).to all( be_a(Commit) )
+ end
- it 'returns only Commit instances' do
- expect(subject).to all( be_a(Commit) )
+ context 'when some commits are not found ' do
+ let(:oids) do
+ ['deadbeef'] + TestEnv::BRANCH_SHA.values.first(10)
end
- context 'when some commits are not found ' do
- let(:oids) do
- ['deadbeef'] + TestEnv::BRANCH_SHA.values.first(10)
- end
-
- it 'returns only found commits' do
- expect(subject).not_to include(nil)
- expect(subject.size).to eq(10)
- end
+ it 'returns only found commits' do
+ expect(subject).not_to include(nil)
+ expect(subject.size).to eq(10)
end
+ end
- context 'when no oids are passed' do
- let(:oids) { [] }
+ context 'when no oids are passed' do
+ let(:oids) { [] }
- it 'does not call #batch_by_oid' do
- expect(Gitlab::Git::Commit).not_to receive(:batch_by_oid)
+ it 'does not call #batch_by_oid' do
+ expect(Gitlab::Git::Commit).not_to receive(:batch_by_oid)
- subject
- end
+ subject
end
end
-
- context 'when Gitaly list_commits_by_oid is enabled' do
- it_behaves_like 'batch commits fetching'
- end
-
- context 'when Gitaly list_commits_by_oid is enabled', :disable_gitaly do
- it_behaves_like 'batch commits fetching'
- end
end
describe '#find_commits_by_message' do
- shared_examples 'finding commits by message' do
- it 'returns commits with messages containing a given string' do
- commit_ids = repository.find_commits_by_message('submodule').map(&:id)
+ it 'returns commits with messages containing a given string' do
+ commit_ids = repository.find_commits_by_message('submodule').map(&:id)
- expect(commit_ids).to include(
- '5937ac0a7beb003549fc5fd26fc247adbce4a52e',
- '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9',
- 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660'
- )
- expect(commit_ids).not_to include('913c66a37b4a45b9769037c55c2d238bd0942d2e')
- end
-
- it 'is case insensitive' do
- commit_ids = repository.find_commits_by_message('SUBMODULE').map(&:id)
-
- expect(commit_ids).to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
- end
+ expect(commit_ids).to include(
+ '5937ac0a7beb003549fc5fd26fc247adbce4a52e',
+ '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9',
+ 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660'
+ )
+ expect(commit_ids).not_to include('913c66a37b4a45b9769037c55c2d238bd0942d2e')
end
- context 'when Gitaly commits_by_message feature is enabled' do
- it_behaves_like 'finding commits by message'
- end
+ it 'is case insensitive' do
+ commit_ids = repository.find_commits_by_message('SUBMODULE').map(&:id)
- context 'when Gitaly commits_by_message feature is disabled', :skip_gitaly_mock do
- it_behaves_like 'finding commits by message'
+ expect(commit_ids).to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
end
describe 'when storage is broken', :broken_storage do
@@ -1328,34 +1267,23 @@ describe Repository do
describe '#merge' do
let(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project) }
-
let(:message) { 'Test \r\n\r\n message' }
- shared_examples '#merge' do
- it 'merges the code and returns the commit id' do
- expect(merge_commit).to be_present
- expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present
- end
-
- it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do
- merge_commit_id = merge(repository, user, merge_request, message)
-
- expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id)
- end
+ it 'merges the code and returns the commit id' do
+ expect(merge_commit).to be_present
+ expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present
+ end
- it 'removes carriage returns from commit message' do
- merge_commit_id = merge(repository, user, merge_request, message)
+ it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do
+ merge_commit_id = merge(repository, user, merge_request, message)
- expect(repository.commit(merge_commit_id).message).to eq(message.delete("\r"))
- end
+ expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id)
end
- context 'with gitaly' do
- it_behaves_like '#merge'
- end
+ it 'removes carriage returns from commit message' do
+ merge_commit_id = merge(repository, user, merge_request, message)
- context 'without gitaly', :skip_gitaly_mock do
- it_behaves_like '#merge'
+ expect(repository.commit(merge_commit_id).message).to eq(message.delete("\r"))
end
def merge(repository, user, merge_request, message)
@@ -1392,98 +1320,78 @@ describe Repository do
end
describe '#revert' do
- shared_examples 'reverting a commit' do
- let(:new_image_commit) { repository.commit('33f3729a45c02fc67d00adb1b8bca394b0e761d9') }
- let(:update_image_commit) { repository.commit('2f63565e7aac07bcdadb654e253078b727143ec4') }
- let(:message) { 'revert message' }
-
- context 'when there is a conflict' do
- it 'raises an error' do
- expect { repository.revert(user, new_image_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
- end
- end
+ let(:new_image_commit) { repository.commit('33f3729a45c02fc67d00adb1b8bca394b0e761d9') }
+ let(:update_image_commit) { repository.commit('2f63565e7aac07bcdadb654e253078b727143ec4') }
+ let(:message) { 'revert message' }
- context 'when commit was already reverted' do
- it 'raises an error' do
- repository.revert(user, update_image_commit, 'master', message)
-
- expect { repository.revert(user, update_image_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
- end
- end
-
- context 'when commit can be reverted' do
- it 'reverts the changes' do
- expect(repository.revert(user, update_image_commit, 'master', message)).to be_truthy
- end
+ context 'when there is a conflict' do
+ it 'raises an error' do
+ expect { repository.revert(user, new_image_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
end
+ end
- context 'reverting a merge commit' do
- it 'reverts the changes' do
- merge_commit
- expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).to be_present
+ context 'when commit was already reverted' do
+ it 'raises an error' do
+ repository.revert(user, update_image_commit, 'master', message)
- repository.revert(user, merge_commit, 'master', message)
- expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).not_to be_present
- end
+ expect { repository.revert(user, update_image_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
end
end
- context 'when Gitaly revert feature is enabled' do
- it_behaves_like 'reverting a commit'
+ context 'when commit can be reverted' do
+ it 'reverts the changes' do
+ expect(repository.revert(user, update_image_commit, 'master', message)).to be_truthy
+ end
end
- context 'when Gitaly revert feature is disabled', :disable_gitaly do
- it_behaves_like 'reverting a commit'
+ context 'reverting a merge commit' do
+ it 'reverts the changes' do
+ merge_commit
+ expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).to be_present
+
+ repository.revert(user, merge_commit, 'master', message)
+ expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).not_to be_present
+ end
end
end
describe '#cherry_pick' do
- shared_examples 'cherry-picking a commit' do
- let(:conflict_commit) { repository.commit('c642fe9b8b9f28f9225d7ea953fe14e74748d53b') }
- let(:pickable_commit) { repository.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
- let(:pickable_merge) { repository.commit('e56497bb5f03a90a51293fc6d516788730953899') }
- let(:message) { 'cherry-pick message' }
-
- context 'when there is a conflict' do
- it 'raises an error' do
- expect { repository.cherry_pick(user, conflict_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
- end
+ let(:conflict_commit) { repository.commit('c642fe9b8b9f28f9225d7ea953fe14e74748d53b') }
+ let(:pickable_commit) { repository.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
+ let(:pickable_merge) { repository.commit('e56497bb5f03a90a51293fc6d516788730953899') }
+ let(:message) { 'cherry-pick message' }
+
+ context 'when there is a conflict' do
+ it 'raises an error' do
+ expect { repository.cherry_pick(user, conflict_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
end
+ end
- context 'when commit was already cherry-picked' do
- it 'raises an error' do
- repository.cherry_pick(user, pickable_commit, 'master', message)
+ context 'when commit was already cherry-picked' do
+ it 'raises an error' do
+ repository.cherry_pick(user, pickable_commit, 'master', message)
- expect { repository.cherry_pick(user, pickable_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
- end
+ expect { repository.cherry_pick(user, pickable_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
end
+ end
- context 'when commit can be cherry-picked' do
- it 'cherry-picks the changes' do
- expect(repository.cherry_pick(user, pickable_commit, 'master', message)).to be_truthy
- end
+ context 'when commit can be cherry-picked' do
+ it 'cherry-picks the changes' do
+ expect(repository.cherry_pick(user, pickable_commit, 'master', message)).to be_truthy
end
+ end
- context 'cherry-picking a merge commit' do
- it 'cherry-picks the changes' do
- expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).to be_nil
+ context 'cherry-picking a merge commit' do
+ it 'cherry-picks the changes' do
+ expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).to be_nil
- cherry_pick_commit_sha = repository.cherry_pick(user, pickable_merge, 'improve/awesome', message)
- cherry_pick_commit_message = project.commit(cherry_pick_commit_sha).message
+ cherry_pick_commit_sha = repository.cherry_pick(user, pickable_merge, 'improve/awesome', message)
+ cherry_pick_commit_message = project.commit(cherry_pick_commit_sha).message
- expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).not_to be_nil
- expect(cherry_pick_commit_message).to eq(message)
- end
+ expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).not_to be_nil
+ expect(cherry_pick_commit_message).to eq(message)
end
end
-
- context 'when Gitaly cherry_pick feature is enabled' do
- it_behaves_like 'cherry-picking a commit'
- end
-
- context 'when Gitaly cherry_pick feature is disabled', :disable_gitaly do
- it_behaves_like 'cherry-picking a commit'
- end
end
describe '#before_delete' do
@@ -2190,33 +2098,23 @@ describe Repository do
let(:commit) { repository.commit }
let(:ancestor) { commit.parents.first }
- shared_examples '#ancestor?' do
- it 'it is an ancestor' do
- expect(repository.ancestor?(ancestor.id, commit.id)).to eq(true)
- end
-
- it 'it is not an ancestor' do
- expect(repository.ancestor?(commit.id, ancestor.id)).to eq(false)
- end
-
- it 'returns false on nil-values' do
- expect(repository.ancestor?(nil, commit.id)).to eq(false)
- expect(repository.ancestor?(ancestor.id, nil)).to eq(false)
- expect(repository.ancestor?(nil, nil)).to eq(false)
- end
+ it 'it is an ancestor' do
+ expect(repository.ancestor?(ancestor.id, commit.id)).to eq(true)
+ end
- it 'returns false for invalid commit IDs' do
- expect(repository.ancestor?(commit.id, Gitlab::Git::BLANK_SHA)).to eq(false)
- expect(repository.ancestor?( Gitlab::Git::BLANK_SHA, commit.id)).to eq(false)
- end
+ it 'it is not an ancestor' do
+ expect(repository.ancestor?(commit.id, ancestor.id)).to eq(false)
end
- context 'with Gitaly enabled' do
- it_behaves_like('#ancestor?')
+ it 'returns false on nil-values' do
+ expect(repository.ancestor?(nil, commit.id)).to eq(false)
+ expect(repository.ancestor?(ancestor.id, nil)).to eq(false)
+ expect(repository.ancestor?(nil, nil)).to eq(false)
end
- context 'with Gitaly disabled', :skip_gitaly_mock do
- it_behaves_like('#ancestor?')
+ it 'returns false for invalid commit IDs' do
+ expect(repository.ancestor?(commit.id, Gitlab::Git::BLANK_SHA)).to eq(false)
+ expect(repository.ancestor?( Gitlab::Git::BLANK_SHA, commit.id)).to eq(false)
end
end
@@ -2403,4 +2301,22 @@ describe Repository do
repository.merge_base('master', 'fix')
end
end
+
+ describe '#cache' do
+ subject(:cache) { repository.send(:cache) }
+
+ it 'returns a RepositoryCache' do
+ expect(subject).to be_kind_of Gitlab::RepositoryCache
+ end
+
+ it 'when is_wiki it includes wiki as part of key' do
+ allow(repository).to receive(:is_wiki) { true }
+
+ expect(subject.namespace).to include('wiki')
+ end
+
+ it 'when is_wiki is false extra_namespace is nil' do
+ expect(subject.namespace).not_to include('wiki')
+ end
+ end
end
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/ssh_host_key_spec.rb b/spec/models/ssh_host_key_spec.rb
new file mode 100644
index 00000000000..75db43b3d56
--- /dev/null
+++ b/spec/models/ssh_host_key_spec.rb
@@ -0,0 +1,164 @@
+require 'spec_helper'
+
+describe SshHostKey do
+ using RSpec::Parameterized::TableSyntax
+ include ReactiveCachingHelpers
+
+ let(:key1) do
+ 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3UpyF2iLqy1d63M6k3jH1vuEnq/NWtE+o' \
+ 'rJe1Xn7JoRbduKd6zpsJ0JhBGWgcQK0ph0aGW5PcudzzBSc+SlYfCc4GTaxDtmj41hW0o72m' \
+ 'NiuDW3oKXXShOiVRde2ZOquH8Z865jGiZIC8BI/bXZD29IGUih0hPu7Rjp70VYiE+35QRf/p' \
+ 'sD0Ddrz8QUIG3A/2dMzLI5F5ZORk3BIX2F3mJwJOvZxRhR/SqyphDMZ5eZ0EzqbFBCDE6HAB' \
+ 'Woz9ck8RBGLvCIggmDHj3FmMLcQGMDiy6wKp7QdnBtxjCP6vtE6YPUM223AqsWt+9NTtCfB8' \
+ 'YdNAH7YcHHOR1FgtSk1x'
+ end
+
+ let(:key2) do
+ 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDLIp+4ciR2YO9f9rpldc7InNQw/TBUtcNb' \
+ 'J2XR0rr15/5ytz7YM16xXG0Qjx576PNSmqs4gbTrvTuFZak+v1Jx/9deHRq/yqp9f+tv33+i' \
+ 'aJGCQCX/+OVY7aWgV2R9YsS7XQ4mnv4XlOTEssib/rGAIT+ATd/GcdYSEOO+dh4O09/6O/jI' \
+ 'MGSeP+NNetgn1nPCnLOjrXFZUnUtNDi6EEKeIlrliJjSb7Jr4f7gjvZnv4RskWHHFo8FgAAq' \
+ 't0gOMT6EmKrnypBe2vLGSAXbtkXr01q6/DNPH+n9VA1LTV6v1KN/W5CN5tQV11wRSKiM8g5O' \
+ 'Ebi86VjJRi2sOuYoXQU1'
+ end
+
+ # Purposefully ordered so that `sort` will make changes
+ let(:known_hosts) do
+ <<~EOF
+ example.com #{key1} git@localhost
+ @revoked other.example.com #{key2} git@localhost
+ EOF
+ end
+
+ let(:extra) { known_hosts + "foo\nbar\n" }
+ let(:reversed) { known_hosts.lines.reverse.join }
+
+ let(:compare_host_keys) { nil }
+
+ def stub_ssh_keyscan(args, status: true, stdout: "", stderr: "")
+ stdin = StringIO.new
+ stdout = double(:stdout, read: stdout)
+ stderr = double(:stderr, read: stderr)
+ wait_thr = double(:wait_thr, value: double(success?: status))
+
+ expect(Open3).to receive(:popen3).with({}, 'ssh-keyscan', *args).and_yield(stdin, stdout, stderr, wait_thr)
+
+ stdin
+ end
+
+ let(:project) { build(:project) }
+
+ subject(:ssh_host_key) { described_class.new(project: project, url: 'ssh://example.com:2222', compare_host_keys: compare_host_keys) }
+
+ describe '#fingerprints', :use_clean_rails_memory_store_caching do
+ it 'returns an array of indexed fingerprints when the cache is filled' do
+ stub_reactive_cache(ssh_host_key, known_hosts: known_hosts)
+
+ expected = [key1, key2]
+ .map { |data| Gitlab::SSHPublicKey.new(data) }
+ .each_with_index
+ .map { |key, i| { bits: key.bits, fingerprint: key.fingerprint, type: key.type, index: i } }
+
+ expect(ssh_host_key.fingerprints.as_json).to eq(expected)
+ end
+
+ it 'returns an empty array when the cache is empty' do
+ expect(ssh_host_key.fingerprints).to eq([])
+ end
+ end
+
+ describe '#fingerprints', :use_clean_rails_memory_store_caching do
+ it 'returns an array of indexed fingerprints when the cache is filled' do
+ stub_reactive_cache(ssh_host_key, known_hosts: known_hosts)
+
+ expect(ssh_host_key.fingerprints.as_json).to eq(
+ [
+ { bits: 2048, fingerprint: Gitlab::SSHPublicKey.new(key1).fingerprint, type: :rsa, index: 0 },
+ { bits: 2048, fingerprint: Gitlab::SSHPublicKey.new(key2).fingerprint, type: :rsa, index: 1 }
+ ]
+ )
+ end
+
+ it 'returns an empty array when the cache is empty' do
+ expect(ssh_host_key.fingerprints).to eq([])
+ end
+ end
+
+ describe '#host_keys_changed?' do
+ where(:known_hosts_a, :known_hosts_b, :result) do
+ known_hosts | extra | true
+ known_hosts | "foo\n" | true
+ known_hosts | '' | true
+ known_hosts | nil | true
+ known_hosts | known_hosts | false
+ reversed | known_hosts | false
+ extra | "foo\n" | true
+ '' | '' | false
+ nil | nil | false
+ '' | nil | false
+ end
+
+ with_them do
+ let(:compare_host_keys) { known_hosts_b }
+
+ subject { ssh_host_key.host_keys_changed? }
+
+ context '(normal)' do
+ let(:compare_host_keys) { known_hosts_b }
+
+ before do
+ expect(ssh_host_key).to receive(:known_hosts).and_return(known_hosts_a)
+ end
+
+ it { is_expected.to eq(result) }
+ end
+
+ # Comparisons should be symmetrical, so test the reverse too
+ context '(reversed)' do
+ let(:compare_host_keys) { known_hosts_a }
+
+ before do
+ expect(ssh_host_key).to receive(:known_hosts).and_return(known_hosts_b)
+ end
+
+ it { is_expected.to eq(result) }
+ end
+ end
+ end
+
+ describe '#calculate_reactive_cache' do
+ subject(:cache) { ssh_host_key.calculate_reactive_cache }
+
+ it 'writes the hostname to STDIN' do
+ stdin = stub_ssh_keyscan(%w[-T 5 -p 2222 -f-])
+
+ cache
+
+ expect(stdin.string).to eq("example.com\n")
+ end
+
+ context 'successful key scan' do
+ it 'stores the cleaned known_hosts data' do
+ stub_ssh_keyscan(%w[-T 5 -p 2222 -f-], stdout: "KEY 1\nKEY 1\n\n# comment\nKEY 2\n")
+
+ is_expected.to eq(known_hosts: "KEY 1\nKEY 2\n")
+ end
+ end
+
+ context 'failed key scan (exit code 1)' do
+ it 'returns a generic error' do
+ stub_ssh_keyscan(%w[-T 5 -p 2222 -f-], stdout: 'blarg', status: false)
+
+ is_expected.to eq(error: 'Failed to detect SSH host keys')
+ end
+ end
+
+ context 'failed key scan (exit code 0)' do
+ it 'returns a generic error' do
+ stub_ssh_keyscan(%w[-T 5 -p 2222 -f-], stderr: 'Unknown host')
+
+ is_expected.to eq(error: 'Failed to detect SSH host keys')
+ end
+ end
+ end
+end
diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb
index 2c01578aaca..82ff2a002e0 100644
--- a/spec/models/todo_spec.rb
+++ b/spec/models/todo_spec.rb
@@ -226,7 +226,7 @@ describe Todo do
create(:todo, target: create(:merge_request))
- expect(described_class.for_type(Issue)).to eq([todo])
+ expect(described_class.for_type(Issue.name)).to eq([todo])
end
end
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 b3474e74aa4..733c1c49f08 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
@@ -730,6 +770,14 @@ describe User do
expect(user.incoming_email_token).not_to be_blank
end
+
+ it 'uses SecureRandom to generate the incoming email token' do
+ expect(SecureRandom).to receive(:hex).and_return('3b8ca303')
+
+ user = create(:user)
+
+ expect(user.incoming_email_token).to eql('gitlab')
+ end
end
describe '#ensure_user_rights_and_limits' do
@@ -1067,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')
@@ -1074,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
@@ -1111,6 +1174,22 @@ describe User do
expect(described_class.by_any_email(user.email, confirmed: true)).to eq([user])
end
+
+ it 'finds user through a private commit email' do
+ user = create(:user)
+ private_email = user.private_commit_email
+
+ expect(described_class.by_any_email(private_email)).to eq([user])
+ expect(described_class.by_any_email(private_email, confirmed: true)).to eq([user])
+ end
+
+ it 'finds user through a private commit email in an array' do
+ user = create(:user)
+ private_email = user.private_commit_email
+
+ expect(described_class.by_any_email([private_email])).to eq([user])
+ expect(described_class.by_any_email([private_email], confirmed: true)).to eq([user])
+ end
end
describe '.search' do
@@ -1438,7 +1517,12 @@ describe User do
email_unconfirmed = create :email, user: user
user.reload
- expect(user.all_emails).to match_array([user.email, email_unconfirmed.email, email_confirmed.email])
+ expect(user.all_emails).to contain_exactly(
+ user.email,
+ user.private_commit_email,
+ email_unconfirmed.email,
+ email_confirmed.email
+ )
end
end
@@ -1449,7 +1533,11 @@ 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 contain_exactly(
+ user.email,
+ user.private_commit_email,
+ email_confirmed.email
+ )
end
end
@@ -1465,6 +1553,18 @@ 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 true for an outdated private commit email' do
+ old_email = user.private_commit_email
+
+ user.update!(username: 'changed-username')
+
+ expect(user.verified_email?(old_email)).to be_truthy
+ end
+
it 'returns false when the email is not verified/confirmed' do
email_unconfirmed = create :email, user: user
user.reload
@@ -1660,6 +1760,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..cba22b2cc4e 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
@@ -189,180 +200,160 @@ describe WikiPage do
end
describe '#create' do
- shared_examples 'create method' do
- context 'with valid attributes' do
- it 'raises an error if a page with the same path already exists' do
- create_page('New Page', 'content')
- create_page('foo/bar', 'content')
- expect { create_page('New Page', 'other content') }.to raise_error Gitlab::Git::Wiki::DuplicatePageError
- expect { create_page('foo/bar', 'other content') }.to raise_error Gitlab::Git::Wiki::DuplicatePageError
-
- destroy_page('New Page')
- destroy_page('bar', 'foo')
- end
+ context 'with valid attributes' do
+ it 'raises an error if a page with the same path already exists' do
+ create_page('New Page', 'content')
+ create_page('foo/bar', 'content')
+ expect { create_page('New Page', 'other content') }.to raise_error Gitlab::Git::Wiki::DuplicatePageError
+ expect { create_page('foo/bar', 'other content') }.to raise_error Gitlab::Git::Wiki::DuplicatePageError
- it 'if the title is preceded by a / it is removed' do
- create_page('/New Page', 'content')
+ destroy_page('New Page')
+ destroy_page('bar', 'foo')
+ end
- expect(wiki.find_page('New Page')).not_to be_nil
+ it 'if the title is preceded by a / it is removed' do
+ create_page('/New Page', 'content')
- destroy_page('New Page')
- end
+ expect(wiki.find_page('New Page')).not_to be_nil
+
+ destroy_page('New Page')
end
end
+ end
- context 'when Gitaly is enabled' do
- it_behaves_like 'create method'
+ describe "#update" do
+ before do
+ create_page("Update", "content")
+ @page = wiki.find_page("Update")
end
- context 'when Gitaly is disabled', :skip_gitaly_mock do
- it_behaves_like 'create method'
+ after do
+ destroy_page(@page.title, @page.directory)
end
- end
- describe "#update" do
- shared_examples 'update method' do
- before do
- create_page("Update", "content")
+ context "with valid attributes" do
+ it "updates the content of the page" do
+ new_content = "new content"
+
+ @page.update(content: new_content)
@page = wiki.find_page("Update")
- end
- after do
- destroy_page(@page.title, @page.directory)
+ expect(@page.content).to eq("new content")
end
- context "with valid attributes" do
- it "updates the content of the page" do
- new_content = "new content"
-
- @page.update(content: new_content)
- @page = wiki.find_page("Update")
-
- expect(@page.content).to eq("new content")
- end
+ it "updates the title of the page" do
+ new_title = "Index v.1.2.4"
- it "updates the title of the page" do
- new_title = "Index v.1.2.4"
+ @page.update(title: new_title)
+ @page = wiki.find_page(new_title)
- @page.update(title: new_title)
- @page = wiki.find_page(new_title)
-
- expect(@page.title).to eq(new_title)
- end
+ expect(@page.title).to eq(new_title)
+ end
- it "returns true" do
- expect(@page.update(content: "more content")).to be_truthy
- end
+ it "returns true" do
+ expect(@page.update(content: "more content")).to be_truthy
end
+ end
- context 'with same last commit sha' do
- it 'returns true' do
- expect(@page.update(content: 'more content', last_commit_sha: @page.last_commit_sha)).to be_truthy
- end
+ context 'with same last commit sha' do
+ it 'returns true' do
+ expect(@page.update(content: 'more content', last_commit_sha: @page.last_commit_sha)).to be_truthy
end
+ end
- context 'with different last commit sha' do
- it 'raises exception' do
- expect { @page.update(content: 'more content', last_commit_sha: 'xxx') }.to raise_error(WikiPage::PageChangedError)
- end
+ context 'with different last commit sha' do
+ it 'raises exception' do
+ expect { @page.update(content: 'more content', last_commit_sha: 'xxx') }.to raise_error(WikiPage::PageChangedError)
end
+ end
- context 'when renaming a page' do
- it 'raises an error if the page already exists' do
- create_page('Existing Page', 'content')
+ context 'when renaming a page' do
+ it 'raises an error if the page already exists' do
+ create_page('Existing Page', 'content')
- expect { @page.update(title: 'Existing Page', content: 'new_content') }.to raise_error(WikiPage::PageRenameError)
- expect(@page.title).to eq 'Update'
- expect(@page.content).to eq 'new_content'
+ expect { @page.update(title: 'Existing Page', content: 'new_content') }.to raise_error(WikiPage::PageRenameError)
+ expect(@page.title).to eq 'Update'
+ expect(@page.content).to eq 'new_content'
- destroy_page('Existing Page')
- end
+ destroy_page('Existing Page')
+ end
- it 'updates the content and rename the file' do
- new_title = 'Renamed Page'
- new_content = 'updated content'
+ it 'updates the content and rename the file' do
+ new_title = 'Renamed Page'
+ new_content = 'updated content'
- expect(@page.update(title: new_title, content: new_content)).to be_truthy
+ expect(@page.update(title: new_title, content: new_content)).to be_truthy
- @page = wiki.find_page(new_title)
+ @page = wiki.find_page(new_title)
- expect(@page).not_to be_nil
- expect(@page.content).to eq new_content
- end
+ expect(@page).not_to be_nil
+ expect(@page.content).to eq new_content
end
+ end
- context 'when moving a page' do
- it 'raises an error if the page already exists' do
- create_page('foo/Existing Page', 'content')
-
- expect { @page.update(title: 'foo/Existing Page', content: 'new_content') }.to raise_error(WikiPage::PageRenameError)
- expect(@page.title).to eq 'Update'
- expect(@page.content).to eq 'new_content'
-
- destroy_page('Existing Page', 'foo')
- end
+ context 'when moving a page' do
+ it 'raises an error if the page already exists' do
+ create_page('foo/Existing Page', 'content')
- it 'updates the content and moves the file' do
- new_title = 'foo/Other Page'
- new_content = 'new_content'
+ expect { @page.update(title: 'foo/Existing Page', content: 'new_content') }.to raise_error(WikiPage::PageRenameError)
+ expect(@page.title).to eq 'Update'
+ expect(@page.content).to eq 'new_content'
- expect(@page.update(title: new_title, content: new_content)).to be_truthy
+ destroy_page('Existing Page', 'foo')
+ end
- page = wiki.find_page(new_title)
+ it 'updates the content and moves the file' do
+ new_title = 'foo/Other Page'
+ new_content = 'new_content'
- expect(page).not_to be_nil
- expect(page.content).to eq new_content
- end
+ expect(@page.update(title: new_title, content: new_content)).to be_truthy
- context 'in subdir' do
- before do
- create_page('foo/Existing Page', 'content')
- @page = wiki.find_page('foo/Existing Page')
- end
+ page = wiki.find_page(new_title)
- it 'moves the page to the root folder if the title is preceded by /', :skip_gitaly_mock do
- expect(@page.slug).to eq 'foo/Existing-Page'
- expect(@page.update(title: '/Existing Page', content: 'new_content')).to be_truthy
- expect(@page.slug).to eq 'Existing-Page'
- end
+ expect(page).not_to be_nil
+ expect(page.content).to eq new_content
+ end
- it 'does nothing if it has the same title' do
- original_path = @page.slug
+ context 'in subdir' do
+ before do
+ create_page('foo/Existing Page', 'content')
+ @page = wiki.find_page('foo/Existing Page')
+ end
- expect(@page.update(title: 'Existing Page', content: 'new_content')).to be_truthy
- expect(@page.slug).to eq original_path
- end
+ it 'moves the page to the root folder if the title is preceded by /' do
+ expect(@page.slug).to eq 'foo/Existing-Page'
+ expect(@page.update(title: '/Existing Page', content: 'new_content')).to be_truthy
+ expect(@page.slug).to eq 'Existing-Page'
end
- context 'in root dir' do
- it 'does nothing if the title is preceded by /' do
- original_path = @page.slug
+ it 'does nothing if it has the same title' do
+ original_path = @page.slug
- expect(@page.update(title: '/Update', content: 'new_content')).to be_truthy
- expect(@page.slug).to eq original_path
- end
+ expect(@page.update(title: 'Existing Page', content: 'new_content')).to be_truthy
+ expect(@page.slug).to eq original_path
end
end
- context "with invalid attributes" do
- it 'aborts update if title blank' do
- expect(@page.update(title: '', content: 'new_content')).to be_falsey
- expect(@page.content).to eq 'new_content'
-
- page = wiki.find_page('Update')
- expect(page.content).to eq 'content'
+ context 'in root dir' do
+ it 'does nothing if the title is preceded by /' do
+ original_path = @page.slug
- @page.title = 'Update'
+ expect(@page.update(title: '/Update', content: 'new_content')).to be_truthy
+ expect(@page.slug).to eq original_path
end
end
end
- context 'when Gitaly is enabled' do
- it_behaves_like 'update method'
- end
+ context "with invalid attributes" do
+ it 'aborts update if title blank' do
+ expect(@page.update(title: '', content: 'new_content')).to be_falsey
+ expect(@page.content).to eq 'new_content'
- context 'when Gitaly is disabled', :skip_gitaly_mock do
- it_behaves_like 'update method'
+ page = wiki.find_page('Update')
+ expect(page.content).to eq 'content'
+
+ @page.title = 'Update'
+ end
end
end
@@ -383,34 +374,24 @@ describe WikiPage do
end
describe "#versions" do
- shared_examples 'wiki page versions' do
- let(:page) { wiki.find_page("Update") }
+ let(:page) { wiki.find_page("Update") }
- before do
- create_page("Update", "content")
- end
-
- after do
- destroy_page("Update")
- end
-
- it "returns an array of all commits for the page" do
- 3.times { |i| page.update(content: "content #{i}") }
-
- expect(page.versions.count).to eq(4)
- end
+ before do
+ create_page("Update", "content")
+ end
- it 'returns instances of WikiPageVersion' do
- expect(page.versions).to all( be_a(Gitlab::Git::WikiPageVersion) )
- end
+ after do
+ destroy_page("Update")
end
- context 'when Gitaly is enabled' do
- it_behaves_like 'wiki page versions'
+ it "returns an array of all commits for the page" do
+ 3.times { |i| page.update(content: "content #{i}") }
+
+ expect(page.versions.count).to eq(4)
end
- context 'when Gitaly is disabled', :disable_gitaly do
- it_behaves_like 'wiki page versions'
+ it 'returns instances of WikiPageVersion' do
+ expect(page.versions).to all( be_a(Gitlab::Git::WikiPageVersion) )
end
end
@@ -544,23 +525,13 @@ describe WikiPage do
end
describe '#formatted_content' do
- shared_examples 'fetching page formatted content' do
- it 'returns processed content of the page' do
- subject.create({ title: "RDoc", content: "*bold*", format: "rdoc" })
- page = wiki.find_page('RDoc')
+ it 'returns processed content of the page' do
+ subject.create({ title: "RDoc", content: "*bold*", format: "rdoc" })
+ page = wiki.find_page('RDoc')
- expect(page.formatted_content).to eq("\n<p><strong>bold</strong></p>\n")
-
- destroy_page('RDoc')
- end
- end
-
- context 'when Gitaly wiki_page_formatted_data is enabled' do
- it_behaves_like 'fetching page formatted content'
- end
+ expect(page.formatted_content).to eq("\n<p><strong>bold</strong></p>\n")
- context 'when Gitaly wiki_page_formatted_data is disabled', :disable_gitaly do
- it_behaves_like 'fetching page formatted content'
+ destroy_page('RDoc')
end
end
diff --git a/spec/policies/ci/pipeline_policy_spec.rb b/spec/policies/ci/pipeline_policy_spec.rb
index bd32faf06ef..8022f61e67d 100644
--- a/spec/policies/ci/pipeline_policy_spec.rb
+++ b/spec/policies/ci/pipeline_policy_spec.rb
@@ -74,5 +74,23 @@ describe Ci::PipelinePolicy, :models do
expect(policy).to be_allowed :update_pipeline
end
end
+
+ describe 'destroy_pipeline' do
+ let(:project) { create(:project, :public) }
+
+ context 'when user has owner access' do
+ let(:user) { project.owner }
+
+ it 'is enabled' do
+ expect(policy).to be_allowed :destroy_pipeline
+ end
+ end
+
+ context 'when user is not owner' do
+ it 'is disabled' do
+ expect(policy).not_to be_allowed :destroy_pipeline
+ end
+ end
+ 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/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb
index a1b52d8692d..bafcddebbb7 100644
--- a/spec/presenters/merge_request_presenter_spec.rb
+++ b/spec/presenters/merge_request_presenter_spec.rb
@@ -403,6 +403,15 @@ describe MergeRequestPresenter do
is_expected
.to eq("<a href=\"/#{resource.source_project.full_path}/tree/#{resource.source_branch}\">#{resource.source_branch}</a>")
end
+
+ it 'escapes html, when source_branch does not exist' do
+ xss_attempt = "<img src='x' onerror=alert('bad stuff') />"
+
+ allow(resource).to receive(:source_branch) { xss_attempt }
+ allow(resource).to receive(:source_branch_exists?) { false }
+
+ is_expected.to eq(ERB::Util.html_escape(xss_attempt))
+ end
end
describe '#rebase_path' do
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/rack_servers/configs/config.ru b/spec/rack_servers/configs/config.ru
new file mode 100644
index 00000000000..63daeb9eec5
--- /dev/null
+++ b/spec/rack_servers/configs/config.ru
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+app = proc do |env|
+ if env['REQUEST_METHOD'] == 'GET'
+ [200, {}, ["#{Process.pid}"]]
+ else
+ Process.kill(env['QUERY_STRING'], Process.pid)
+ [200, {}, ['Bye!']]
+ end
+end
+
+run app
diff --git a/spec/rack_servers/configs/puma.rb b/spec/rack_servers/configs/puma.rb
new file mode 100644
index 00000000000..d6b6d83d648
--- /dev/null
+++ b/spec/rack_servers/configs/puma.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+# Note: this file is used for testing puma in `spec/rack_servers/puma_spec.rb` only
+# Note: as per the convention in `config/puma.example.development.rb`,
+# this file will replace `/home/git` with the actual working directory
+
+directory '/home/git'
+threads 1, 10
+queue_requests false
+pidfile '/home/git/gitlab/tmp/pids/puma.pid'
+bind 'unix:///home/git/gitlab/tmp/tests/puma.socket'
+workers 1
+preload_app!
+worker_timeout 60
+
+require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events"
+require_relative "/home/git/gitlab/lib/gitlab/cluster/puma_worker_killer_initializer"
+
+before_fork do
+ Gitlab::Cluster::PumaWorkerKillerInitializer.start @config.options
+ Gitlab::Cluster::LifecycleEvents.do_before_fork
+end
+
+Gitlab::Cluster::LifecycleEvents.set_puma_options @config.options
+on_worker_boot do
+ Gitlab::Cluster::LifecycleEvents.do_worker_start
+ File.write('/home/git/gitlab/tmp/tests/puma-worker-ready', Process.pid)
+end
+
+on_restart do
+ Gitlab::Cluster::LifecycleEvents.do_master_restart
+end
diff --git a/spec/rack_servers/puma_spec.rb b/spec/rack_servers/puma_spec.rb
new file mode 100644
index 00000000000..431fab87857
--- /dev/null
+++ b/spec/rack_servers/puma_spec.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require 'fileutils'
+
+require 'excon'
+
+require 'spec_helper'
+
+describe 'Puma' do
+ before(:all) do
+ project_root = File.expand_path('../..', __dir__)
+
+ config_lines = File.read('spec/rack_servers/configs/puma.rb')
+ .gsub('/home/git/gitlab', project_root)
+ .gsub('/home/git', project_root)
+
+ config_path = File.join(project_root, "tmp/tests/puma.rb")
+ @socket_path = File.join(project_root, 'tmp/tests/puma.socket')
+
+ File.write(config_path, config_lines)
+
+ cmd = %W[puma -e test -C #{config_path} #{File.join(__dir__, 'configs/config.ru')}]
+ @puma_master_pid = spawn(*cmd)
+ wait_puma_boot!(@puma_master_pid, File.join(project_root, 'tmp/tests/puma-worker-ready'))
+ WebMock.allow_net_connect!
+ end
+
+ %w[SIGQUIT SIGTERM SIGKILL].each do |signal|
+ it "has a worker that self-terminates on signal #{signal}" do
+ response = Excon.get('unix://', socket: @socket_path)
+ expect(response.status).to eq(200)
+
+ worker_pid = response.body.to_i
+ expect(worker_pid).to be > 0
+
+ begin
+ Excon.post("unix://?#{signal}", socket: @socket_path)
+ rescue Excon::Error::Socket
+ # The connection may be closed abruptly
+ end
+
+ expect(pid_gone?(worker_pid)).to eq(true)
+ end
+ end
+
+ after(:all) do
+ begin
+ WebMock.disable_net_connect!(allow_localhost: true)
+ Process.kill('TERM', @puma_master_pid)
+ rescue Errno::ESRCH
+ end
+ end
+
+ def wait_puma_boot!(master_pid, ready_file)
+ # We have seen the boot timeout after 2 minutes in CI so let's set it to 5 minutes.
+ timeout = 5 * 60
+ timeout.times do
+ return if File.exist?(ready_file)
+
+ pid = Process.waitpid(master_pid, Process::WNOHANG)
+ raise "puma failed to boot: #{$?}" unless pid.nil?
+
+ sleep 1
+ end
+
+ raise "puma boot timed out after #{timeout} seconds"
+ end
+
+ def pid_gone?(pid)
+ # Worker termination should take less than a second. That makes 10
+ # seconds a generous timeout.
+ 10.times do
+ begin
+ Process.kill(0, pid)
+ rescue Errno::ESRCH
+ return true
+ end
+
+ sleep 1
+ end
+
+ false
+ end
+end
diff --git a/spec/unicorn/unicorn_spec.rb b/spec/rack_servers/unicorn_spec.rb
index a4cf479a339..6a02ebcd048 100644
--- a/spec/unicorn/unicorn_spec.rb
+++ b/spec/rack_servers/unicorn_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'fileutils'
require 'excon'
@@ -6,12 +8,16 @@ require 'spec_helper'
describe 'Unicorn' do
before(:all) do
- config_lines = File.read('config/unicorn.rb.example').split("\n")
+ project_root = File.expand_path('../..', __dir__)
+
+ config_lines = File.read('config/unicorn.rb.example')
+ .gsub('/home/git/gitlab', project_root)
+ .gsub('/home/git', project_root)
+ .split("\n")
# Remove these because they make setup harder.
config_lines = config_lines.reject do |line|
%w[
- working_directory
worker_processes
listen
pid
@@ -26,33 +32,18 @@ describe 'Unicorn' do
# predictable which process will handle our requests.
config_lines << 'worker_processes 1'
- @socket_path = File.join(Dir.pwd, 'tmp/tests/unicorn.socket')
+ @socket_path = File.join(project_root, 'tmp/tests/unicorn.socket')
config_lines << "listen '#{@socket_path}'"
- ready_file = 'tmp/tests/unicorn-worker-ready'
+ ready_file = File.join(project_root, 'tmp/tests/unicorn-worker-ready')
FileUtils.rm_f(ready_file)
after_fork_index = config_lines.index { |l| l.start_with?('after_fork') }
config_lines.insert(after_fork_index + 1, "File.write('#{ready_file}', Process.pid)")
- config_path = 'tmp/tests/unicorn.rb'
+ config_path = File.join(project_root, 'tmp/tests/unicorn.rb')
File.write(config_path, config_lines.join("\n") + "\n")
- rackup_path = 'tmp/tests/config.ru'
- File.write(rackup_path, <<~EOS)
- app =
- proc do |env|
- if env['REQUEST_METHOD'] == 'GET'
- [200, {}, [Process.pid]]
- else
- Process.kill(env['QUERY_STRING'], Process.pid)
- [200, {}, ['Bye!']]
- end
- end
-
- run app
- EOS
-
- cmd = %W[unicorn -E test -c #{config_path} #{rackup_path}]
+ cmd = %W[unicorn -E test -c #{config_path} spec/rack_servers/configs/config.ru]
@unicorn_master_pid = spawn(*cmd)
wait_unicorn_boot!(@unicorn_master_pid, ready_file)
WebMock.allow_net_connect!
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 e0b5b34f9c4..2ebcb787d06 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -494,6 +494,24 @@ describe API::Internal do
end
end
+ context 'request times out' do
+ context 'git push' do
+ it 'responds with a gateway timeout' do
+ personal_project = create(:project, namespace: user.namespace)
+
+ expect_next_instance_of(Gitlab::GitAccess) do |access|
+ expect(access).to receive(:check).and_raise(Gitlab::GitAccess::TimeoutError, "Foo")
+ end
+ push(key, personal_project)
+
+ expect(response).to have_gitlab_http_status(503)
+ expect(json_response['status']).to be_falsey
+ expect(json_response['message']).to eq("Foo")
+ expect(user.reload.last_activity_on).to be_nil
+ end
+ end
+ end
+
context "archived project" do
before do
project.add_developer(user)
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 9f6cf12f9a7..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)
@@ -178,16 +178,44 @@ describe API::Issues do
expect(first_issue['id']).to eq(issue2.id)
end
- it 'returns issues reacted by the authenticated user by the given emoji' do
- issue2 = create(:issue, project: project, author: user, assignees: [user])
- award_emoji = create(:award_emoji, awardable: issue2, user: user2, name: 'star')
+ it 'returns issues with no assignee' do
+ issue2 = create(:issue, author: user2, project: project)
- get api('/issues', user2), my_reaction_emoji: award_emoji.name, scope: 'all'
+ get api('/issues', user), assignee_id: 'None', scope: 'all'
expect_paginated_array_response(size: 1)
expect(first_issue['id']).to eq(issue2.id)
end
+ it 'returns issues with any assignee' do
+ # This issue without assignee should not be returned
+ create(:issue, author: user2, project: project)
+
+ get api('/issues', user), assignee_id: 'Any', scope: 'all'
+
+ expect_paginated_array_response(size: 3)
+ end
+
+ it 'returns issues reacted by the authenticated user' do
+ issue2 = create(:issue, project: project, author: user, assignees: [user])
+ create(:award_emoji, awardable: issue2, user: user2, name: 'star')
+
+ create(:award_emoji, awardable: issue, user: user2, name: 'thumbsup')
+
+ 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
get api("/issues", user), search: issue.title
@@ -1773,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/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 07d19e3ad29..e4e0ca285e0 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -143,6 +143,23 @@ describe API::MergeRequests do
expect_response_ordered_exactly(merge_request3)
end
+ it 'returns an array of merge requests with no assignee' do
+ merge_request3 = create(:merge_request, :simple, author: user, source_project: project2, target_project: project2, source_branch: 'other-branch')
+
+ get api('/merge_requests', user), assignee_id: 'None', scope: :all
+
+ expect_response_ordered_exactly(merge_request3)
+ end
+
+ it 'returns an array of merge requests with any assignee' do
+ # This MR with no assignee should not be returned
+ create(:merge_request, :simple, author: user, source_project: project2, target_project: project2, source_branch: 'other-branch')
+
+ get api('/merge_requests', user), assignee_id: 'Any', scope: :all
+
+ expect_response_contain_exactly(merge_request, merge_request2, merge_request_closed, merge_request_merged, merge_request_locked)
+ end
+
it 'returns an array of merge requests assigned to me' do
merge_request3 = create(:merge_request, :simple, author: user, assignee: user2, source_project: project2, target_project: project2, source_branch: 'other-branch')
diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb
index f0e1992bccd..638cc9767d4 100644
--- a/spec/requests/api/pipelines_spec.rb
+++ b/spec/requests/api/pipelines_spec.rb
@@ -438,6 +438,67 @@ describe API::Pipelines do
end
end
+ describe 'DELETE /projects/:id/pipelines/:pipeline_id' do
+ context 'authorized user' do
+ let(:owner) { project.owner }
+
+ it 'destroys the pipeline' do
+ delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", owner)
+
+ expect(response).to have_gitlab_http_status(204)
+ expect { pipeline.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'returns 404 when it does not exist' do
+ delete api("/projects/#{project.id}/pipelines/123456", owner)
+
+ expect(response).to have_gitlab_http_status(404)
+ expect(json_response['message']).to eq '404 Not found'
+ end
+
+ it 'logs an audit event' do
+ expect { delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", owner) }.to change { SecurityEvent.count }.by(1)
+ end
+
+ context 'when the pipeline has jobs' do
+ let!(:build) { create(:ci_build, project: project, pipeline: pipeline) }
+
+ it 'destroys associated jobs' do
+ delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", owner)
+
+ expect(response).to have_gitlab_http_status(204)
+ expect { build.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ context 'when user is not member' do
+ it 'should return a 404' do
+ delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member)
+
+ expect(response).to have_gitlab_http_status(404)
+ expect(json_response['message']).to eq '404 Project Not Found'
+ end
+ end
+
+ context 'when user is developer' do
+ let(:developer) { create(:user) }
+
+ before do
+ project.add_developer(developer)
+ end
+
+ it 'should return a 403' do
+ delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", developer)
+
+ expect(response).to have_gitlab_http_status(403)
+ expect(json_response['message']).to eq '403 Forbidden'
+ end
+ end
+ end
+ end
+
describe 'POST /projects/:id/pipelines/:pipeline_id/retry' do
context 'authorized user' do
let!(:pipeline) do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 22e5a7c7174..62b6a3ce42e 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -200,6 +200,24 @@ describe API::Projects do
expect(json_response.first).to include 'statistics'
end
+ it "does not include license by default" do
+ get api('/projects', user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first).not_to include('license', 'license_url')
+ end
+
+ it "does not include license if requested" do
+ get api('/projects', user), license: true
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first).not_to include('license', 'license_url')
+ end
+
context 'when external issue tracker is enabled' do
let!(:jira_service) { create(:jira_service, project: project) }
@@ -994,6 +1012,26 @@ describe API::Projects do
})
end
+ it "does not include license fields by default" do
+ get api("/projects/#{project.id}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).not_to include('license', 'license_url')
+ end
+
+ it 'includes license fields when requested' do
+ get api("/projects/#{project.id}", user), license: true
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['license']).to eq({
+ 'key' => project.repository.license.key,
+ 'name' => project.repository.license.name,
+ 'nickname' => project.repository.license.nickname,
+ 'html_url' => project.repository.license.url,
+ 'source_url' => project.repository.license.meta['source']
+ })
+ end
+
it "does not include statistics by default" do
get api("/projects/#{project.id}", user)
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index 43ceb332cfb..909703a8d47 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -797,6 +797,24 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
it { expect(job).to be_runner_system_failure }
end
+
+ context 'when failure_reason is unrecognized value' do
+ before do
+ update_job(state: 'failed', failure_reason: 'what_is_this')
+ job.reload
+ end
+
+ it { expect(job).to be_unknown_failure }
+ end
+
+ context 'when failure_reason is job_execution_timeout' do
+ before do
+ update_job(state: 'failed', failure_reason: 'job_execution_timeout')
+ job.reload
+ end
+
+ it { expect(job).to be_job_execution_timeout }
+ end
end
context 'when trace is given' do
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/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index e6d01c9689f..bb913ae0e79 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -2018,11 +2018,11 @@ describe API::Users do
expect(json_response['message']).to eq('403 Forbidden')
end
- it 'returns a personal access token' do
+ it 'returns an impersonation token' do
get api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", admin)
expect(response).to have_gitlab_http_status(200)
- expect(json_response['token']).to be_present
+ expect(json_response['token']).not_to be_present
expect(json_response['impersonation']).to be_truthy
end
end
diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb
index c40d01e1a14..08bada44178 100644
--- a/spec/requests/api/wikis_spec.rb
+++ b/spec/requests/api/wikis_spec.rb
@@ -158,6 +158,16 @@ describe API::Wikis do
expect(json_response.size).to eq(1)
expect(json_response['error']).to eq('file is missing')
end
+
+ it 'responds with validation error on invalid temp file' do
+ payload[:file] = { tempfile: '/etc/hosts' }
+
+ post(api(url, user), payload)
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response.size).to eq(1)
+ expect(json_response['error']).to eq('file is invalid')
+ end
end
describe 'GET /projects/:id/wikis' do
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 6894c65d639..8a6a38fe5f8 100644
--- a/spec/serializers/environment_status_entity_spec.rb
+++ b/spec/serializers/environment_status_entity_spec.rb
@@ -4,17 +4,18 @@ 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) }
- let(:environment_status) { EnvironmentStatus.new(environment, merge_request) }
+ let(:environment_status) { EnvironmentStatus.new(environment, merge_request, merge_request.diff_head_sha) }
let(:entity) { described_class.new(environment_status, request: request) }
subject { entity.as_json }
before do
+ deployment.update(sha: merge_request.diff_head_sha)
allow(request).to receive(:current_user).and_return(user)
end
@@ -26,24 +27,53 @@ describe EnvironmentStatusEntity do
it { is_expected.to include(:deployed_at) }
it { is_expected.to include(:deployed_at_formatted) }
it { is_expected.to include(:changes) }
+ it { is_expected.to include(:status) }
it { is_expected.not_to include(:stop_url) }
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/serializers/merge_request_widget_entity_spec.rb b/spec/serializers/merge_request_widget_entity_spec.rb
index 5bf8aa7f23f..561421d5ac8 100644
--- a/spec/serializers/merge_request_widget_entity_spec.rb
+++ b/spec/serializers/merge_request_widget_entity_spec.rb
@@ -52,6 +52,40 @@ describe MergeRequestWidgetEntity do
end
end
+ describe 'merge_pipeline' do
+ it 'returns nil' do
+ expect(subject[:merge_pipeline]).to be_nil
+ end
+
+ context 'when is merged' do
+ let(:resource) { create(:merged_merge_request, source_project: project, merge_commit_sha: project.commit.id) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: resource.target_branch, sha: resource.merge_commit_sha) }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'returns merge_pipeline' do
+ pipeline.reload
+ pipeline_payload = PipelineDetailsEntity
+ .represent(pipeline, request: request)
+ .as_json
+
+ expect(subject[:merge_pipeline]).to eq(pipeline_payload)
+ end
+
+ context 'when user cannot read pipelines on target project' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'returns nil' do
+ expect(subject[:merge_pipeline]).to be_nil
+ end
+ end
+ end
+ end
+
describe 'metrics' do
context 'when metrics record exists with merged data' do
before do
diff --git a/spec/services/boards/visits/create_service_spec.rb b/spec/services/boards/visits/create_service_spec.rb
new file mode 100644
index 00000000000..6baf7ac9deb
--- /dev/null
+++ b/spec/services/boards/visits/create_service_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Boards::Visits::CreateService do
+ describe '#execute' do
+ let(:user) { create(:user) }
+
+ context 'when a project board' do
+ let(:project) { create(:project) }
+ let(:project_board) { create(:board, project: project) }
+
+ subject(:service) { described_class.new(project_board.parent, user) }
+
+ it 'returns nil when there is no user' do
+ service.current_user = nil
+
+ expect(service.execute(project_board)).to eq nil
+ end
+
+ it 'returns nil when database is read only' do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+
+ expect(service.execute(project_board)).to eq nil
+ end
+
+ it 'records the visit' do
+ expect(BoardProjectRecentVisit).to receive(:visited!).once
+
+ service.execute(project_board)
+ end
+ end
+
+ context 'when a group board' do
+ let(:group) { create(:group) }
+ let(:group_board) { create(:board, group: group) }
+
+ subject(:service) { described_class.new(group_board.parent, user) }
+
+ it 'returns nil when there is no user' do
+ service.current_user = nil
+
+ expect(service.execute(group_board)).to eq nil
+ end
+
+ it 'records the visit' do
+ expect(BoardGroupRecentVisit).to receive(:visited!).once
+
+ service.execute(group_board)
+ end
+ end
+ end
+end
diff --git a/spec/services/boards/visits/latest_service_spec.rb b/spec/services/boards/visits/latest_service_spec.rb
new file mode 100644
index 00000000000..e55d599e2cc
--- /dev/null
+++ b/spec/services/boards/visits/latest_service_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Boards::Visits::LatestService do
+ describe '#execute' do
+ let(:user) { create(:user) }
+
+ context 'when a project board' do
+ let(:project) { create(:project) }
+ let(:project_board) { create(:board, project: project) }
+
+ subject(:service) { described_class.new(project_board.parent, user) }
+
+ it 'returns nil when there is no user' do
+ service.current_user = nil
+
+ expect(service.execute).to eq nil
+ end
+
+ it 'queries for most recent visit' do
+ expect(BoardProjectRecentVisit).to receive(:latest).once
+
+ service.execute
+ end
+ end
+
+ context 'when a group board' do
+ let(:group) { create(:group) }
+ let(:group_board) { create(:board, group: group) }
+
+ subject(:service) { described_class.new(group_board.parent, user) }
+
+ it 'returns nil when there is no user' do
+ service.current_user = nil
+
+ expect(service.execute).to eq nil
+ end
+
+ it 'queries for most recent visit' do
+ expect(BoardGroupRecentVisit).to receive(:latest).once
+
+ service.execute
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 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/destroy_pipeline_service_spec.rb b/spec/services/ci/destroy_pipeline_service_spec.rb
new file mode 100644
index 00000000000..097daf67feb
--- /dev/null
+++ b/spec/services/ci/destroy_pipeline_service_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ::Ci::DestroyPipelineService do
+ let(:project) { create(:project) }
+ let!(:pipeline) { create(:ci_pipeline, project: project) }
+
+ subject { described_class.new(project, user).execute(pipeline) }
+
+ context 'user is owner' do
+ let(:user) { project.owner }
+
+ it 'destroys the pipeline' do
+ subject
+
+ expect { pipeline.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'logs an audit event' do
+ expect { subject }.to change { SecurityEvent.count }.by(1)
+ end
+
+ context 'when the pipeline has jobs' do
+ let!(:build) { create(:ci_build, project: project, pipeline: pipeline) }
+
+ it 'destroys associated jobs' do
+ subject
+
+ expect { build.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'destroys associated stages' do
+ stages = pipeline.stages
+
+ subject
+
+ expect(stages).to all(raise_error(ActiveRecord::RecordNotFound))
+ end
+
+ context 'when job has artifacts' do
+ let!(:artifact) { create(:ci_job_artifact, :archive, job: build) }
+
+ it 'destroys associated artifacts' do
+ subject
+
+ expect { artifact.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+ end
+ end
+
+ context 'user is not owner' do
+ let(:user) { create(:user) }
+
+ it 'raises an exception' do
+ expect { subject }.to raise_error(Gitlab::Access::AccessDeniedError)
+ 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..661364ac765
--- /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
+ create(: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 065d021db5e..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,95 +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,
- ['api', 'apis/rbac.authorization.k8s.io'],
- 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 c543de21d5b..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,57 +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,
- ['api', 'apis/rbac.authorization.k8s.io'],
- 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/create_from_issue_service_spec.rb b/spec/services/merge_requests/create_from_issue_service_spec.rb
index b1882df732d..393299cce00 100644
--- a/spec/services/merge_requests/create_from_issue_service_spec.rb
+++ b/spec/services/merge_requests/create_from_issue_service_spec.rb
@@ -61,7 +61,15 @@ describe MergeRequests::CreateFromIssueService do
expect(project.repository.branch_exists?(custom_source_branch)).to be_truthy
end
- it 'creates a system note' do
+ it 'creates the new_merge_request system note' do
+ expect(SystemNoteService).to receive(:new_merge_request).with(issue, project, user, instance_of(MergeRequest))
+
+ service.execute
+ end
+
+ it 'creates the new_issue_branch system note when the branch could be created but the merge_request cannot be created' do
+ project.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED)
+
expect(SystemNoteService).to receive(:new_issue_branch).with(issue, project, user, issue.to_branch_name)
service.execute
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 2536c6e2514..61c6ba7d550 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -306,6 +306,66 @@ describe MergeRequests::RefreshService do
end
end
+ context 'forked projects with the same source branch name as target branch' do
+ let!(:first_commit) do
+ @fork_project.repository.create_file(@user, 'test1.txt', 'Test data',
+ message: 'Test commit',
+ branch_name: 'master')
+ end
+ let!(:second_commit) do
+ @fork_project.repository.create_file(@user, 'test2.txt', 'More test data',
+ message: 'Second test commit',
+ branch_name: 'master')
+ end
+ let!(:forked_master_mr) do
+ create(:merge_request,
+ source_project: @fork_project,
+ source_branch: 'master',
+ target_branch: 'master',
+ target_project: @project)
+ end
+ let(:force_push_commit) { @project.commit('feature').id }
+
+ it 'should reload a new diff for a push to the forked project' do
+ expect do
+ service.new(@fork_project, @user).execute(@oldrev, first_commit, 'refs/heads/master')
+ reload_mrs
+ end.to change { forked_master_mr.merge_request_diffs.count }.by(1)
+ end
+
+ it 'should reload a new diff for a force push to the source branch' do
+ expect do
+ service.new(@fork_project, @user).execute(@oldrev, force_push_commit, 'refs/heads/master')
+ reload_mrs
+ end.to change { forked_master_mr.merge_request_diffs.count }.by(1)
+ end
+
+ it 'should reload a new diff for a force push to the target branch' do
+ expect do
+ service.new(@project, @user).execute(@oldrev, force_push_commit, 'refs/heads/master')
+ reload_mrs
+ end.to change { forked_master_mr.merge_request_diffs.count }.by(1)
+ end
+
+ it 'should reload a new diff for a push to the target project that contains a commit in the MR' do
+ expect do
+ service.new(@project, @user).execute(@oldrev, first_commit, 'refs/heads/master')
+ reload_mrs
+ end.to change { forked_master_mr.merge_request_diffs.count }.by(1)
+ end
+
+ it 'should not increase the diff count for a new push to target branch' do
+ new_commit = @project.repository.create_file(@user, 'new-file.txt', 'A new file',
+ message: 'This is a test',
+ branch_name: 'master')
+
+ expect do
+ service.new(@project, @user).execute(@newrev, new_commit, 'refs/heads/master')
+ reload_mrs
+ end.not_to change { forked_master_mr.merge_request_diffs.count }
+ end
+ end
+
context 'push to origin repo target branch after fork project was removed' do
before do
@fork_project.destroy
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..938764f40b0 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,46 @@ 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
+
+ context '/create_merge_request command' do
+ let(:branch_name) { '1-feature' }
+ let(:content) { "/create_merge_request #{branch_name}" }
+ let(:issuable) { issue }
+
+ context 'if issuable is not an Issue' do
+ let(:issuable) { merge_request }
+
+ it_behaves_like 'empty command'
+ end
+
+ context "when logged user cannot create_merge_requests in the project" do
+ let(:project) { create(:project, :archived) }
+
+ it_behaves_like 'empty command'
+ end
+
+ context 'when logged user cannot push code to the project' do
+ let(:project) { create(:project, :private) }
+ let(:service) { described_class.new(project, create(:user)) }
+
+ it_behaves_like 'empty command'
+ end
+
+ it 'populates create_merge_request with branch_name and issue iid' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(create_merge_request: { branch_name: branch_name, issue_iid: issuable.iid })
+ end
+ end
end
describe '#explain' do
@@ -1395,7 +1435,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
@@ -1464,5 +1504,27 @@ describe QuickActions::InterpretService do
end
end
end
+
+ describe 'create a merge request' do
+ context 'with no branch name' do
+ let(:content) { '/create_merge_request' }
+
+ it 'uses the default branch name' do
+ _, explanations = service.explain(content, issue)
+
+ expect(explanations).to eq(['Creates a branch and a merge request to resolve this issue'])
+ end
+ end
+
+ context 'with a branch name' do
+ let(:content) { '/create_merge_request foo' }
+
+ it 'uses the given branch name' do
+ _, explanations = service.explain(content, issue)
+
+ expect(explanations).to eq(["Creates branch 'foo' and a merge request to resolve this issue"])
+ end
+ end
+ end
end
end
diff --git a/spec/services/resource_events/merge_into_notes_service_spec.rb b/spec/services/resource_events/merge_into_notes_service_spec.rb
index 0d333d541c9..c76f6e6f77e 100644
--- a/spec/services/resource_events/merge_into_notes_service_spec.rb
+++ b/spec/services/resource_events/merge_into_notes_service_spec.rb
@@ -66,5 +66,14 @@ describe ResourceEvents::MergeIntoNotesService do
expect(notes.count).to eq 1
expect(notes.first.discussion_id).to eq event.discussion_id
end
+
+ it "preloads the note author's status" do
+ event = create_event(created_at: time)
+ create(:user_status, user: event.user)
+
+ notes = described_class.new(resource, user).execute
+
+ expect(notes.first.author.association(:status)).to be_loaded
+ end
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/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb
index e0335880e8e..81b2c17fdb5 100644
--- a/spec/services/system_hooks_service_spec.rb
+++ b/spec/services/system_hooks_service_spec.rb
@@ -32,7 +32,7 @@ describe SystemHooksService do
end
it do
- project.old_path_with_namespace = 'transfered_from_path'
+ project.old_path_with_namespace = 'transferred_from_path'
expect(event_data(project, :transfer)).to include(
:event_name, :name, :created_at, :updated_at, :path, :project_id,
:owner_name, :owner_email, :project_visibility,
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index a18126ee339..0fbfcb34e50 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -432,6 +432,20 @@ describe SystemNoteService do
end
end
+ describe '.new_merge_request' do
+ subject { described_class.new_merge_request(noteable, project, author, merge_request) }
+
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'merge' }
+ end
+
+ it 'sets the new merge request note text' do
+ expect(subject.note).to eq("created merge request #{merge_request.to_reference} to address this issue")
+ end
+ end
+
describe '.cross_reference' do
subject { described_class.cross_reference(noteable, mentioner, author) }
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..b7b9817efdb 100644
--- a/spec/services/users/build_service_spec.rb
+++ b/spec/services/users/build_service_spec.rb
@@ -8,7 +8,7 @@ describe Users::BuildService do
context 'with an admin user' do
let(:admin_user) { create(:admin) }
- let(:service) { described_class.new(admin_user, params) }
+ let(:service) { described_class.new(admin_user, ActionController::Parameters.new(params).permit!) }
it 'returns a valid user' do
expect(service.execute).to be_valid
@@ -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/features/variable_list_shared_examples.rb b/spec/support/features/variable_list_shared_examples.rb
index f7f851eb1eb..bce1fb01355 100644
--- a/spec/support/features/variable_list_shared_examples.rb
+++ b/spec/support/features/variable_list_shared_examples.rb
@@ -5,7 +5,7 @@ shared_examples 'variable list' do
end
end
- it 'adds new secret variable' do
+ it 'adds new CI variable' do
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('key')
find('.js-ci-variable-input-value').set('key value')
diff --git a/spec/support/gitaly.rb b/spec/support/gitaly.rb
deleted file mode 100644
index 614aaa73693..00000000000
--- a/spec/support/gitaly.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-RSpec.configure do |config|
- config.before(:each) do |example|
- if example.metadata[:disable_gitaly]
- # Use 'and_wrap_original' to make sure the arguments are valid
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_wrap_original { |m, *args| m.call(*args) && false }
- else
- next if example.metadata[:skip_gitaly_mock]
-
- # Use 'and_wrap_original' to make sure the arguments are valid
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_wrap_original do |m, *args|
- m.call(*args)
- !Gitlab::GitalyClient.explicit_opt_in_required.include?(args.first)
- end
- end
- end
-end
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/devise_helpers.rb b/spec/support/helpers/devise_helpers.rb
index 66874e10f38..d32bc2424c0 100644
--- a/spec/support/helpers/devise_helpers.rb
+++ b/spec/support/helpers/devise_helpers.rb
@@ -8,8 +8,15 @@ module DeviseHelpers
end
def env_from_context(context)
+ # When we modify env_config, that is on the global
+ # Rails.application, and we need to stub it and allow it to be
+ # modified in-place, without polluting later tests.
if context.respond_to?(:env_config)
- context.env_config
+ context.env_config.deep_dup.tap do |env|
+ allow(context).to receive(:env_config).and_return(env)
+ end
+ # When we modify env, then the context is a request, or something
+ # else that only lives for a single spec.
elsif context.respond_to?(:env)
context.env
end
diff --git a/spec/support/helpers/filter_item_select_helper.rb b/spec/support/helpers/filter_item_select_helper.rb
deleted file mode 100644
index 519e84d359e..00000000000
--- a/spec/support/helpers/filter_item_select_helper.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# Helper allows you to select value from filter-items
-#
-# Params
-# value - value for select
-# selector - css selector of item
-#
-# Usage:
-#
-# filter_item_select('Any Author', '.js-author-search')
-#
-module FilterItemSelectHelper
- def filter_item_select(value, selector)
- find(selector).click
- wait_for_requests
- page.within('.dropdown-content') do
- click_link value
- end
- end
-end
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/helpers/user_login_helper.rb b/spec/support/helpers/user_login_helper.rb
new file mode 100644
index 00000000000..36c002f53af
--- /dev/null
+++ b/spec/support/helpers/user_login_helper.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module UserLoginHelper
+ def ensure_tab_pane_correctness(visit_path = true)
+ if visit_path
+ visit new_user_session_path
+ end
+
+ ensure_tab_pane_counts
+ ensure_one_active_tab
+ ensure_one_active_pane
+ end
+
+ def ensure_tab_pane_counts
+ tabs_count = page.all('[role="tab"]').size
+ expect(page).to have_selector('[role="tabpanel"]', count: tabs_count)
+ end
+
+ def ensure_one_active_tab
+ expect(page).to have_selector('ul.new-session-tabs > li > a.active', count: 1)
+ end
+
+ def ensure_one_active_pane
+ expect(page).to have_selector('.tab-pane.active', count: 1)
+ end
+end
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/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/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
index 82f0dd5d00f..c391cc48f4e 100644
--- a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
+++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
@@ -44,10 +44,40 @@ shared_examples 'cluster application status specs' do |application_name|
subject { create(application_name, :installing) }
it 'is installed' do
- subject.make_installed
+ subject.make_installed!
expect(subject).to be_installed
end
+
+ it 'updates helm version' do
+ subject.cluster.application_helm.update!(version: '1.2.3')
+
+ subject.make_installed!
+
+ subject.cluster.application_helm.reload
+
+ expect(subject.cluster.application_helm.version).to eq(Gitlab::Kubernetes::Helm::HELM_VERSION)
+ end
+ end
+
+ describe '#make_updated' do
+ subject { create(application_name, :updating) }
+
+ it 'is updated' do
+ subject.make_updated!
+
+ expect(subject).to be_updated
+ end
+
+ it 'updates helm version' do
+ subject.cluster.application_helm.update!(version: '1.2.3')
+
+ subject.make_updated!
+
+ subject.cluster.application_helm.reload
+
+ expect(subject.cluster.application_helm.version).to eq(Gitlab::Kubernetes::Helm::HELM_VERSION)
+ end
end
describe '#make_errored' do
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/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb
index f17c5ac6aac..05b4fb49ea3 100644
--- a/spec/workers/emails_on_push_worker_spec.rb
+++ b/spec/workers/emails_on_push_worker_spec.rb
@@ -101,7 +101,7 @@ describe EmailsOnPushWorker, :mailer do
context "when there are multiple recipients" do
before do
- # This is a hack because we modify the mail object before sending, for efficency,
+ # This is a hack because we modify the mail object before sending, for efficiency,
# but the TestMailer adapter just appends the objects to an array. To clone a mail
# object, create a new one!
# https://github.com/mikel/mail/issues/314#issuecomment-12750108
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index cd6661f09a1..9176eb12b12 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -6,7 +6,7 @@ describe PostReceive do
let(:base64_changes) { Base64.encode64(wrongly_encoded_changes) }
let(:gl_repository) { "project-#{project.id}" }
let(:key) { create(:key, user: project.owner) }
- let(:key_id) { key.shell_id }
+ let!(:key_id) { key.shell_id }
let(:project) do
create(:project, :repository, auto_cancel_pending_pipelines: 'disabled')
@@ -31,85 +31,108 @@ describe PostReceive do
end
describe "#process_project_changes" do
- before do
- allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner)
+ context 'empty changes' do
+ it "does not call any PushService but runs after project hooks" do
+ expect(GitPushService).not_to receive(:new)
+ expect(GitTagPushService).not_to receive(:new)
+ expect_next_instance_of(SystemHooksService) { |service| expect(service).to receive(:execute_hooks) }
+
+ described_class.new.perform(gl_repository, key_id, "")
+ end
end
- context "branches" do
- let(:changes) { "123456 789012 refs/heads/tést" }
+ context 'unidentified user' do
+ let!(:key_id) { "" }
- it "calls GitTagPushService" do
- expect_any_instance_of(GitPushService).to receive(:execute).and_return(true)
- expect_any_instance_of(GitTagPushService).not_to receive(:execute)
- described_class.new.perform(gl_repository, key_id, base64_changes)
+ it 'returns false' do
+ expect(GitPushService).not_to receive(:new)
+ expect(GitTagPushService).not_to receive(:new)
+
+ expect(described_class.new.perform(gl_repository, key_id, base64_changes)).to be false
end
end
- context "tags" do
- let(:changes) { "123456 789012 refs/tags/tag" }
+ context 'with changes' do
+ before do
+ allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner)
+ end
+
+ context "branches" do
+ let(:changes) { "123456 789012 refs/heads/tést" }
- it "calls GitTagPushService" do
- expect_any_instance_of(GitPushService).not_to receive(:execute)
- expect_any_instance_of(GitTagPushService).to receive(:execute).and_return(true)
- described_class.new.perform(gl_repository, key_id, base64_changes)
+ it "calls GitPushService" do
+ expect_any_instance_of(GitPushService).to receive(:execute).and_return(true)
+ expect_any_instance_of(GitTagPushService).not_to receive(:execute)
+ described_class.new.perform(gl_repository, key_id, base64_changes)
+ end
end
- end
- context "merge-requests" do
- let(:changes) { "123456 789012 refs/merge-requests/123" }
+ context "tags" do
+ let(:changes) { "123456 789012 refs/tags/tag" }
- it "does not call any of the services" do
- expect_any_instance_of(GitPushService).not_to receive(:execute)
- expect_any_instance_of(GitTagPushService).not_to receive(:execute)
- described_class.new.perform(gl_repository, key_id, base64_changes)
+ it "calls GitTagPushService" do
+ expect_any_instance_of(GitPushService).not_to receive(:execute)
+ expect_any_instance_of(GitTagPushService).to receive(:execute).and_return(true)
+ described_class.new.perform(gl_repository, key_id, base64_changes)
+ end
end
- end
- context "gitlab-ci.yml" do
- let(:changes) { "123456 789012 refs/heads/feature\n654321 210987 refs/tags/tag" }
+ context "merge-requests" do
+ let(:changes) { "123456 789012 refs/merge-requests/123" }
- subject { described_class.new.perform(gl_repository, key_id, base64_changes) }
+ it "does not call any of the services" do
+ expect_any_instance_of(GitPushService).not_to receive(:execute)
+ expect_any_instance_of(GitTagPushService).not_to receive(:execute)
+ described_class.new.perform(gl_repository, key_id, base64_changes)
+ end
+ end
- context "creates a Ci::Pipeline for every change" do
- before do
- stub_ci_pipeline_to_return_yaml_file
+ context "gitlab-ci.yml" do
+ let(:changes) { "123456 789012 refs/heads/feature\n654321 210987 refs/tags/tag" }
- allow_any_instance_of(Project)
- .to receive(:commit)
- .and_return(project.commit)
+ subject { described_class.new.perform(gl_repository, key_id, base64_changes) }
- allow_any_instance_of(Repository)
- .to receive(:branch_exists?)
- .and_return(true)
- end
+ context "creates a Ci::Pipeline for every change" do
+ before do
+ stub_ci_pipeline_to_return_yaml_file
- it { expect { subject }.to change { Ci::Pipeline.count }.by(2) }
- end
+ allow_any_instance_of(Project)
+ .to receive(:commit)
+ .and_return(project.commit)
- context "does not create a Ci::Pipeline" do
- before do
- stub_ci_pipeline_yaml_file(nil)
+ allow_any_instance_of(Repository)
+ .to receive(:branch_exists?)
+ .and_return(true)
+ end
+
+ it { expect { subject }.to change { Ci::Pipeline.count }.by(2) }
end
- it { expect { subject }.not_to change { Ci::Pipeline.count } }
+ context "does not create a Ci::Pipeline" do
+ before do
+ stub_ci_pipeline_yaml_file(nil)
+ end
+
+ it { expect { subject }.not_to change { Ci::Pipeline.count } }
+ end
end
- end
- context 'after project changes hooks' do
- let(:changes) { '123456 789012 refs/heads/tést' }
- let(:fake_hook_data) { Hash.new(event_name: 'repository_update') }
+ context 'after project changes hooks' do
+ let(:changes) { '123456 789012 refs/heads/tést' }
+ let(:fake_hook_data) { Hash.new(event_name: 'repository_update') }
- before do
- allow_any_instance_of(Gitlab::DataBuilder::Repository).to receive(:update).and_return(fake_hook_data)
- # silence hooks so we can isolate
- allow_any_instance_of(Key).to receive(:post_create_hook).and_return(true)
- allow_any_instance_of(GitPushService).to receive(:execute).and_return(true)
- end
+ before do
+ allow_any_instance_of(Gitlab::DataBuilder::Repository).to receive(:update).and_return(fake_hook_data)
+ # silence hooks so we can isolate
+ allow_any_instance_of(Key).to receive(:post_create_hook).and_return(true)
+ allow_any_instance_of(GitPushService).to receive(:execute).and_return(true)
+ end
- it 'calls SystemHooksService' do
- expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(fake_hook_data, :repository_update_hooks).and_return(true)
+ it 'calls SystemHooksService' do
+ expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(fake_hook_data, :repository_update_hooks).and_return(true)
- described_class.new.perform(gl_repository, key_id, base64_changes)
+ described_class.new.perform(gl_repository, key_id, base64_changes)
+ 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/jupyter/values.yaml b/vendor/jupyter/values.yaml
index 049ffcc3407..24136a7aca5 100644
--- a/vendor/jupyter/values.yaml
+++ b/vendor/jupyter/values.yaml
@@ -16,7 +16,7 @@ singleuser:
lifecycleHooks:
postStart:
exec:
- command: ["git", "clone", "https://gitlab.com/gitlab-org/nurtch-demo.git", "DevOps-Runbook-Demo"]
+ command: ["sh", "-c", "git clone https://gitlab.com/gitlab-org/nurtch-demo.git DevOps-Runbook-Demo || true"]
ingress:
enabled: true
diff --git a/vendor/licenses.csv b/vendor/licenses.csv
index 9083fe076c3..f6fd1efaa83 100644
--- a/vendor/licenses.csv
+++ b/vendor/licenses.csv
@@ -67,8 +67,8 @@
@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/svgs,1.35.0,MIT
+@gitlab/ui,1.11.0,MIT
@sindresorhus/is,0.7.0,MIT
@types/jquery,2.0.48,MIT
@vue/component-compiler-utils,2.2.0,MIT
@@ -133,7 +133,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 +160,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 +262,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 +276,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 +288,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 +296,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 +328,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 +369,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 +447,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 +457,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 +471,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 +567,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 +616,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 +630,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 +652,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 +680,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 +754,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 +800,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 +823,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 +924,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 +944,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 +971,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 +1009,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/vendor/prometheus/values.yaml b/vendor/prometheus/values.yaml
index c432be72163..02ec3e2d9fe 100644
--- a/vendor/prometheus/values.yaml
+++ b/vendor/prometheus/values.yaml
@@ -1,5 +1,7 @@
alertmanager:
enabled: false
+ image:
+ tag: v0.15.2
kubeStateMetrics:
enabled: true
@@ -16,7 +18,7 @@ rbac:
server:
fullnameOverride: "prometheus-prometheus-server"
image:
- tag: v2.1.0
+ tag: v2.4.3
serverFiles:
alerts: {}
diff --git a/yarn.lock b/yarn.lock
index 5da401c1d43..3ee1b2525be 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -616,25 +616,6 @@
lodash "^4.17.10"
to-fast-properties "^2.0.0"
-"@gitlab-org/gitlab-svgs@^1.23.0":
- version "1.32.0"
- 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.8.0":
- version "1.8.0"
- resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-ui/-/gitlab-ui-1.8.0.tgz#dee33d78f68c91644273dbd51734b796108263ee"
- integrity sha512-Owm8bkP4vEihiLD3pmMw1r+UWr3WYGaGUtj0JcwaAg3d05ZneozFEZjazIOWeYTcFsk+ZvNmSk1UA+ARIauhgQ==
- dependencies:
- "@gitlab-org/gitlab-svgs" "^1.23.0"
- bootstrap-vue "^2.0.0-rc.11"
- vue "^2.5.16"
-
"@gitlab/eslint-config@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@gitlab/eslint-config/-/eslint-config-1.1.0.tgz#9757764b3a78b6bacfbcd9533331cb6345ffdd59"
@@ -648,6 +629,26 @@
eslint-plugin-promise "^4.0.1"
eslint-plugin-vue "^5.0.0-beta.3"
+"@gitlab/svgs@^1.38.0":
+ version "1.38.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.38.0.tgz#e2f6e73379d60c7c63af4df8242a94c4671a1dfe"
+ integrity sha512-Mzv6PxVbWEPvvMgXHaGxk8UE1Gard2gifca6loLgfLH7BtjXfESiZyJdQkkTSeBYp5MoqQa88Kw+vJYobwjsSw==
+
+"@gitlab/ui@^1.11.0":
+ version "1.11.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-1.11.0.tgz#b771c2c3d627cf9efbe98c71ee5739624f2ff51f"
+ integrity sha512-hGMHM45kcv9725R6G+n/HxvF3KfVb9oBGRNf1+4n3xAGmtXJ2NlPdIXIsDaye3EeVF9PTOtjLuaqrcp6AGNqZg==
+ dependencies:
+ babel-standalone "^6.26.0"
+ bootstrap-vue "^2.0.0-rc.11"
+ copy-to-clipboard "^3.0.8"
+ highlight.js "^9.13.1"
+ js-beautify "^1.8.8"
+ lodash "^4.17.11"
+ url-search-params-polyfill "^5.0.0"
+ vue "^2.5.16"
+ vue-loader "^15.4.2"
+
"@sindresorhus/is@^0.7.0":
version "0.7.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
@@ -682,11 +683,21 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.5.2.tgz#f19f05314d5421fe37e74153254201a7bf00a707"
integrity sha512-m9zXmifkZsMHZBOyxZWilMwmTlpC8x5Ty360JKTiXvlXZfBWYpsg9ZZvP/Ye+iZUh+Q+MxDLjItVTWIsfwz+8Q==
+"@types/node@^10.11.7":
+ version "10.12.9"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.9.tgz#a07bfa74331471e1dc22a47eb72026843f7b95c8"
+ integrity sha512-eajkMXG812/w3w4a1OcBlaTwsFPO5F7fJ/amy+tieQxEMWBlbV1JGSjkFM+zkHNf81Cad+dfIRA+IBkvmvdAeA==
+
"@types/parse5@^5":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.0.tgz#9ae2106efc443d7c1e26570aa8247828c9c80f11"
integrity sha512-J5D3z703XTDIGQFYXsnU9uRCW9e9mMEFO0Kpe6kykyiboqziru/RlZ0hM2P+PKTG4NHG1SjLrqae/NrV2iJApQ==
+"@types/semver@^5.5.0":
+ version "5.5.0"
+ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45"
+ integrity sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==
+
"@vue/component-compiler-utils@^2.0.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-2.2.0.tgz#bbbb7ed38a9a8a7c93abe7ef2e54a90a04b631b4"
@@ -1214,6 +1225,11 @@ babel-runtime@^6.22.0, babel-runtime@^6.26.0:
core-js "^2.4.0"
regenerator-runtime "^0.11.0"
+babel-standalone@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-standalone/-/babel-standalone-6.26.0.tgz#15fb3d35f2c456695815ebf1ed96fe7f015b6886"
+ integrity sha1-Ffs9NfLEVmlYFevx7Zb+fwFbaIY=
+
babel-template@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02"
@@ -1874,6 +1890,11 @@ commander@2, commander@^2.18.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-2.18.0.tgz#2bf063ddee7c7891176981a2cc798e5754bc6970"
integrity sha512-6CYPa+JP2ftfRU2qkDK+UTVeQYosOg/2GbcjIcKPHfinyOLPVGXu/ovN86RP49Re5ndJK1N0kuiidFFuepc4ZQ==
+commander@^2.19.0:
+ version "2.19.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a"
+ integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==
+
commander@~2.13.0:
version "2.13.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c"
@@ -1956,6 +1977,14 @@ concat-stream@^1.5.0:
readable-stream "^2.2.2"
typedarray "^0.0.6"
+config-chain@~1.1.5:
+ version "1.1.12"
+ resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa"
+ integrity sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==
+ dependencies:
+ ini "^1.3.4"
+ proto-list "~1.2.1"
+
configstore@^3.0.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.1.tgz#094ee662ab83fad9917678de114faaea8fcdca90"
@@ -2056,6 +2085,13 @@ copy-descriptor@^0.1.0:
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
+copy-to-clipboard@^3.0.8:
+ version "3.0.8"
+ resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.0.8.tgz#f4e82f4a8830dce4666b7eb8ded0c9bcc313aba9"
+ integrity sha512-c3GdeY8qxCHGezVb1EFQfHYK/8NZRemgcTIzPq7PuxjHAf/raKibn2QdhHPb/y6q74PMgH6yizaDZlRmw6QyKw==
+ dependencies:
+ toggle-selection "^1.0.3"
+
core-js@^2.2.0, core-js@^2.4.0, core-js@^2.4.1:
version "2.5.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e"
@@ -2862,6 +2898,18 @@ editions@^1.3.3:
resolved "https://registry.yarnpkg.com/editions/-/editions-1.3.4.tgz#3662cb592347c3168eb8e498a0ff73271d67f50b"
integrity sha512-gzao+mxnYDzIysXKMQi/+M1mjy/rjestjg6OPoYTtI+3Izp23oiGZitsl9lPDPiTGXbcSIk1iJWhliSaglxnUg==
+editorconfig@^0.15.0:
+ version "0.15.2"
+ resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.15.2.tgz#047be983abb9ab3c2eefe5199cb2b7c5689f0702"
+ integrity sha512-GWjSI19PVJAM9IZRGOS+YKI8LN+/sjkSjNyvxL5ucqP9/IqtYNXBaQ/6c/hkPNYQHyOHra2KoXZI/JVpuqwmcQ==
+ dependencies:
+ "@types/node" "^10.11.7"
+ "@types/semver" "^5.5.0"
+ commander "^2.19.0"
+ lru-cache "^4.1.3"
+ semver "^5.6.0"
+ sigmund "^1.0.1"
+
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -4083,6 +4131,11 @@ he@^1.1.0, he@^1.1.1:
resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0=
+highlight.js@^9.13.1:
+ version "9.13.1"
+ resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.13.1.tgz#054586d53a6863311168488a0f58d6c505ce641e"
+ integrity sha512-Sc28JNQNDzaH6PORtRLMvif9RSn1mYuOoX3omVjnb0+HbpPygU2ALBI0R/wsiqCb4/fcp07Gdo8g+fhtFrQl6A==
+
hmac-drbg@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
@@ -4814,6 +4867,16 @@ jquery.waitforimages@^2.2.0:
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.3.1.tgz#958ce29e81c9790f31be7792df5d4d95fc57fbca"
integrity sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==
+js-beautify@^1.8.8:
+ version "1.8.8"
+ resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.8.8.tgz#1eb175b73a3571a5f1ed8d98e7cf2b05bfa98471"
+ integrity sha512-qVNq7ZZ7ZbLdzorvSlRDadS0Rh5oyItaE95v6I4wbbuSiijxn7SnnsV6dvKlcXuO2jX7lK8tn9fBulx34K/Ejg==
+ dependencies:
+ config-chain "~1.1.5"
+ editorconfig "^0.15.0"
+ mkdirp "~0.5.0"
+ nopt "~4.0.1"
+
js-cookie@^2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.1.3.tgz#48071625217ac9ecfab8c343a13d42ec09ff0526"
@@ -5171,7 +5234,7 @@ lodash.upperfirst@4.3.1:
resolved "https://registry.yarnpkg.com/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz#1365edf431480481ef0d1c68957a5ed99d49f7ce"
integrity sha1-E2Xt9DFIBIHvDRxolXpe2Z1J984=
-lodash@^4.17.10, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0, lodash@^4.5.0:
+lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0, lodash@^4.5.0:
version "4.17.11"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
@@ -5502,12 +5565,7 @@ mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0:
dependencies:
minimist "0.0.8"
-moment@2.x:
- version "2.19.2"
- resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.2.tgz#8a7f774c95a64550b4c7ebd496683908f9419dbe"
- integrity sha512-Rf6jiHPEfxp9+dlzxPTmRHbvoFXsh2L/U8hOupUMpnuecHQmI6cF6lUbJl3QqKPko1u6ujO+FxtcajLVfLpAtA==
-
-moment@^2.21.0:
+moment@2.x, moment@^2.21.0:
version "2.22.2"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66"
integrity sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=
@@ -5707,7 +5765,7 @@ nopt@3.x:
dependencies:
abbrev "1"
-nopt@^4.0.1:
+nopt@^4.0.1, nopt@~4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=
@@ -6399,6 +6457,11 @@ promise-inflight@^1.0.1:
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
+proto-list@~1.2.1:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
+ integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=
+
proxy-addr@~2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.4.tgz#ecfc733bf22ff8c6f407fa275327b9ab67e48b93"
@@ -6992,6 +7055,11 @@ semver-diff@^2.0.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477"
integrity sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==
+semver@^5.6.0:
+ version "5.6.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
+ integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==
+
send@0.16.2:
version "0.16.2"
resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1"
@@ -7119,6 +7187,11 @@ shebang-regex@^1.0.0:
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
+sigmund@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590"
+ integrity sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=
+
signal-exit@^3.0.0, signal-exit@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
@@ -7729,6 +7802,11 @@ to-regex@^3.0.1, to-regex@^3.0.2:
regex-not "^1.0.2"
safe-regex "^1.1.0"
+toggle-selection@^1.0.3:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
+ integrity sha1-bkWxJj8gF/oKzH2J14sVuL932jI=
+
touch@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b"
@@ -7981,6 +8059,11 @@ url-parse@^1.1.8:
querystringify "~1.0.0"
requires-port "1.0.x"
+url-search-params-polyfill@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/url-search-params-polyfill/-/url-search-params-polyfill-5.0.0.tgz#09b98337c89dcf6c6a6a0bfeb096f6ba83b7526b"
+ integrity sha512-+SCD22QJp4UnqPOI5UTTR0Ljuh8cHbjEf1lIiZrZ8nHTlTixqwVsVQTSfk5vrmDz7N09/Y+ka5jQr0ff35FnQQ==
+
url-to-options@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9"